logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
use agent::SharedThread;
use gpui::{BackgroundExecutor, TestAppContext};
use rpc::proto;
use uuid::Uuid;
use crate::TestServer;
#[gpui::test]
async fn test_share_and_retrieve_thread(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
executor.run_until_parked();
let session_id = Uuid::new_v4().to_string();
let original_thread = SharedThread {
title: "Shared Test Thread".into(),
messages: vec![],
updated_at: chrono::Utc::now(),
model: None,
version: SharedThread::VERSION.to_string(),
};
let thread_data = original_thread
.to_bytes()
.expect("Failed to serialize thread");
client_a
.client()
.request(proto::ShareAgentThread {
session_id: session_id.clone(),
title: original_thread.title.to_string(),
thread_data,
})
.await
.expect("Failed to share thread");
let get_response = client_b
.client()
.request(proto::GetSharedAgentThread {
session_id: session_id.clone(),
})
.await
.expect("Failed to get shared thread");
let imported_shared_thread =
SharedThread::from_bytes(&get_response.thread_data).expect("Failed to deserialize thread");
assert_eq!(imported_shared_thread.title, original_thread.title);
assert_eq!(imported_shared_thread.version, SharedThread::VERSION);
let db_thread = imported_shared_thread.to_db_thread();
assert!(
db_thread.title.starts_with("🔗"),
"Imported thread title should have link prefix"
);
assert!(
db_thread.title.contains("Shared Test Thread"),
"Imported thread should preserve original title"
);
}
#[gpui::test]
async fn test_reshare_updates_existing_thread(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
executor.run_until_parked();
let session_id = Uuid::new_v4().to_string();
client_a
.client()
.request(proto::ShareAgentThread {
session_id: session_id.clone(),
title: "Original Title".to_string(),
thread_data: b"original data".to_vec(),
})
.await
.expect("Failed to share thread");
client_a
.client()
.request(proto::ShareAgentThread {
session_id: session_id.clone(),
title: "Updated Title".to_string(),
thread_data: b"updated data".to_vec(),
})
.await
.expect("Failed to re-share thread");
let get_response = client_b
.client()
.request(proto::GetSharedAgentThread {
session_id: session_id.clone(),
})
.await
.expect("Failed to get shared thread");
assert_eq!(get_response.title, "Updated Title");
assert_eq!(get_response.thread_data, b"updated data".to_vec());
}
#[gpui::test]
async fn test_get_nonexistent_thread(executor: BackgroundExecutor, cx: &mut TestAppContext) {
let mut server = TestServer::start(executor.clone()).await;
let client = server.create_client(cx, "user_a").await;
executor.run_until_parked();
let nonexistent_session_id = Uuid::new_v4().to_string();
let result = client
.client()
.request(proto::GetSharedAgentThread {
session_id: nonexistent_session_id,
})
.await;
assert!(result.is_err(), "Should fail for nonexistent thread");
}
#[gpui::test]
async fn test_sync_imported_thread(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
executor.run_until_parked();
let session_id = Uuid::new_v4().to_string();
// User A shares a thread with initial content.
let initial_thread = SharedThread {
title: "Initial Title".into(),
messages: vec![],
updated_at: chrono::Utc::now(),
model: None,
version: SharedThread::VERSION.to_string(),
};
client_a
.client()
.request(proto::ShareAgentThread {
session_id: session_id.clone(),
title: initial_thread.title.to_string(),
thread_data: initial_thread.to_bytes().expect("Failed to serialize"),
})
.await
.expect("Failed to share thread");
// User B imports the thread.
let initial_response = client_b
.client()
.request(proto::GetSharedAgentThread {
session_id: session_id.clone(),
})
.await
.expect("Failed to get shared thread");
let initial_imported =
SharedThread::from_bytes(&initial_response.thread_data).expect("Failed to deserialize");
assert_eq!(initial_imported.title.as_ref(), "Initial Title");
// User A updates the shared thread.
let updated_thread = SharedThread {
title: "Updated Title".into(),
messages: vec![],
updated_at: chrono::Utc::now(),
model: None,
version: SharedThread::VERSION.to_string(),
};
client_a
.client()
.request(proto::ShareAgentThread {
session_id: session_id.clone(),
title: updated_thread.title.to_string(),
thread_data: updated_thread.to_bytes().expect("Failed to serialize"),
})
.await
.expect("Failed to re-share thread");
// User B syncs the imported thread (fetches the latest version).
let synced_response = client_b
.client()
.request(proto::GetSharedAgentThread {
session_id: session_id.clone(),
})
.await
.expect("Failed to sync shared thread");
let synced_thread =
SharedThread::from_bytes(&synced_response.thread_data).expect("Failed to deserialize");
// The synced thread should have the updated title.
assert_eq!(synced_thread.title.as_ref(), "Updated Title");
}

View File

@@ -0,0 +1,484 @@
use crate::TestServer;
use call::ActiveCall;
use client::ChannelId;
use gpui::{App, BackgroundExecutor, Entity, TestAppContext, TestScreenCaptureSource};
use project::Project;
use rpc::proto::PeerId;
use workspace::{AutoWatch, SharedScreen, Workspace};
use super::TestClient;
struct AutoWatchTestSetup {
client_a: TestClient,
client_b: TestClient,
client_c: TestClient,
channel_id: ChannelId,
user_a_project: Entity<Project>,
user_b_project: Entity<Project>,
}
async fn setup_auto_watch_test(
server: &mut TestServer,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) -> AutoWatchTestSetup {
setup_auto_watch_test_with_initial_participants(server, user_a, user_b, user_c, true).await
}
async fn setup_auto_watch_late_joiner_test(
server: &mut TestServer,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) -> AutoWatchTestSetup {
setup_auto_watch_test_with_initial_participants(server, user_a, user_b, user_c, false).await
}
async fn setup_auto_watch_test_with_initial_participants(
server: &mut TestServer,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
join_user_c: bool,
) -> AutoWatchTestSetup {
let client_a = server.create_client(user_a, "user_a").await;
let client_b = server.create_client(user_b, "user_b").await;
let client_c = server.create_client(user_c, "user_c").await;
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, user_a),
&mut [(&client_b, user_b), (&client_c, user_c)],
)
.await;
let user_a_project = client_a.build_empty_local_project(false, user_a);
let user_b_project = client_b.build_empty_local_project(false, user_b);
let active_call_a = user_a.read(ActiveCall::global);
active_call_a
.update(user_a, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
let active_call_b = user_b.read(ActiveCall::global);
active_call_b
.update(user_b, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
if join_user_c {
let active_call_c = user_c.read(ActiveCall::global);
active_call_c
.update(user_c, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
}
AutoWatchTestSetup {
client_a,
client_b,
client_c,
channel_id,
user_a_project,
user_b_project,
}
}
#[gpui::test]
async fn test_auto_watch_opens_existing_share_on_toggle(
executor: BackgroundExecutor,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
let (workspace_a, user_a) = setup
.client_a
.build_workspace(&setup.user_a_project, user_a);
executor.run_until_parked();
start_screen_share(user_b).await;
executor.run_until_parked();
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_b.peer_id().unwrap(),
cx,
);
});
}
#[gpui::test]
async fn test_auto_watch_opens_share_when_no_one_is_sharing_yet(
executor: BackgroundExecutor,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
let (workspace_a, user_a) = setup
.client_a
.build_workspace(&setup.user_a_project, user_a);
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
start_screen_share(user_b).await;
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_b.peer_id().unwrap(),
cx,
);
});
}
#[gpui::test]
async fn test_auto_watch_switches_to_next_share_on_share_end(
executor: BackgroundExecutor,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
let (workspace_a, user_a) = setup
.client_a
.build_workspace(&setup.user_a_project, user_a);
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
start_screen_share(user_b).await;
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_b.peer_id().unwrap(),
cx,
);
});
start_screen_share(user_c).await;
executor.run_until_parked();
stop_screen_share(user_b);
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_c.peer_id().unwrap(),
cx,
);
});
}
#[gpui::test]
async fn test_auto_watch_ignores_shares_while_user_is_sharing(
executor: BackgroundExecutor,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
let (workspace_a, user_a) = setup
.client_a
.build_workspace(&setup.user_a_project, user_a);
start_screen_share(user_a).await;
executor.run_until_parked();
start_screen_share(user_b).await;
executor.run_until_parked();
// Should NOT open B's screen cause we are sharing
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_no_screen_share_tabs_exist(
workspace,
"should not open anyone's screen share when toggling on while sharing",
cx,
);
});
}
#[gpui::test]
async fn test_auto_watch_opens_share_after_local_user_stops_sharing(
executor: BackgroundExecutor,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
let (workspace_a, user_a) = setup
.client_a
.build_workspace(&setup.user_a_project, user_a);
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
start_screen_share(user_a).await;
executor.run_until_parked();
start_screen_share(user_b).await;
executor.run_until_parked();
stop_screen_share(user_a);
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_b.peer_id().unwrap(),
cx,
);
});
}
#[gpui::test]
async fn test_auto_watch_toggle_off_leaves_tabs_open(
executor: BackgroundExecutor,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
let (workspace_a, user_a) = setup
.client_a
.build_workspace(&setup.user_a_project, user_a);
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
start_screen_share(user_b).await;
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_b.peer_id().unwrap(),
cx,
);
});
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_b.peer_id().unwrap(),
cx,
);
});
}
#[gpui::test]
async fn test_auto_watch_reopens_screen_share_from_returning_channel_participant(
executor: BackgroundExecutor,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let setup = setup_auto_watch_late_joiner_test(&mut server, user_a, user_b, user_c).await;
let (workspace_a, user_a) = setup
.client_a
.build_workspace(&setup.user_a_project, user_a);
let (workspace_b, user_b) = setup
.client_b
.build_workspace(&setup.user_b_project, user_b);
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
workspace_b.update_in(user_b, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
executor.run_until_parked();
let active_call_c = user_c.read(ActiveCall::global);
active_call_c
.update(user_c, |call, cx| call.join_channel(setup.channel_id, cx))
.await
.unwrap();
executor.run_until_parked();
start_screen_share(user_c).await;
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_c.peer_id().unwrap(),
cx,
);
});
workspace_b.update(user_b, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_c.peer_id().unwrap(),
cx,
);
});
active_call_c
.update(user_c, |call, cx| call.hang_up(cx))
.await
.unwrap();
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_no_screen_share_tabs_exist(
workspace,
"user A should stop seeing user C's screen after user C hangs up",
cx,
);
});
workspace_b.update(user_b, |workspace, cx| {
assert_no_screen_share_tabs_exist(
workspace,
"user B should stop seeing user C's screen after user C hangs up",
cx,
);
});
let active_call_c = user_c.read(ActiveCall::global);
active_call_c
.update(user_c, |call, cx| call.join_channel(setup.channel_id, cx))
.await
.unwrap();
executor.run_until_parked();
start_screen_share(user_c).await;
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_c.peer_id().unwrap(),
cx,
);
});
workspace_b.update(user_b, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_c.peer_id().unwrap(),
cx,
);
});
}
#[gpui::test]
async fn test_auto_watch_is_disabled_when_following_collaborator(
executor: BackgroundExecutor,
user_a: &mut TestAppContext,
user_b: &mut TestAppContext,
user_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
let (workspace_a, user_a) = setup
.client_a
.build_workspace(&setup.user_a_project, user_a);
let user_b_peer_id = setup.client_b.peer_id().unwrap();
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.toggle_auto_watch(window, cx);
});
start_screen_share(user_b).await;
executor.run_until_parked();
workspace_a.update(user_a, |workspace, cx| {
assert_active_item_is_screen_share_for_peer(
workspace,
setup.client_b.peer_id().unwrap(),
cx,
);
});
workspace_a.update_in(user_a, |workspace, window, cx| {
workspace.follow(user_b_peer_id, window, cx);
});
executor.run_until_parked();
workspace_a.update(user_a, |workspace, _cx| {
assert_eq!(*workspace.auto_watch_state(), AutoWatch::Off);
});
}
#[track_caller]
fn assert_no_screen_share_tabs_exist(workspace: &Workspace, message: &str, cx: &App) {
let has_shared_screen_tab = workspace
.active_pane()
.read(cx)
.items()
.any(|item| item.downcast::<SharedScreen>().is_some());
assert!(!has_shared_screen_tab, "{message}");
}
#[track_caller]
fn assert_active_item_is_screen_share_for_peer(workspace: &Workspace, peer_id: PeerId, cx: &App) {
let active_item = workspace.active_item(cx).expect("no active item");
let shared_screen = active_item
.downcast::<SharedScreen>()
.expect("expected active item to be a shared screen");
assert_eq!(shared_screen.read(cx).peer_id, peer_id);
}
async fn start_screen_share(cx: &mut TestAppContext) {
let display = TestScreenCaptureSource::new();
cx.set_screen_capture_sources(vec![display]);
let screen = cx
.update(|cx| cx.screen_capture_sources())
.await
.unwrap()
.unwrap()
.into_iter()
.next()
.unwrap();
let active_call = cx.read(ActiveCall::global);
active_call
.update(cx, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(screen, cx))
})
.await
.unwrap();
}
#[track_caller]
fn stop_screen_share(cx: &mut TestAppContext) {
let active_call = cx.read(ActiveCall::global);
active_call
.update(cx, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.unshare_screen(true, cx))
})
.unwrap();
}

View File

@@ -0,0 +1,884 @@
use crate::{TestServer, test_server::open_channel_notes};
use call::ActiveCall;
use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL;
use client::{Collaborator, LegacyUserId, ParticipantIndex};
use collab::rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT};
use collab_ui::channel_view::ChannelView;
use collections::HashMap;
use editor::{Anchor, Editor, MultiBufferOffset, ToOffset};
use futures::future;
use gpui::{BackgroundExecutor, Context, Entity, TestAppContext, Window};
use rpc::{RECEIVE_TIMEOUT, proto::PeerId};
use serde_json::json;
use std::ops::Range;
use util::rel_path::rel_path;
use workspace::CollaboratorId;
#[gpui::test]
async fn test_core_channel_buffers(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_id = server
.make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
.await;
// Client A joins the channel buffer
let channel_buffer_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
// Client A edits the buffer
let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
buffer_a.update(cx_a, |buffer, cx| {
buffer.edit([(0..0, "hello world")], None, cx)
});
buffer_a.update(cx_a, |buffer, cx| {
buffer.edit([(5..5, ", cruel")], None, cx)
});
buffer_a.update(cx_a, |buffer, cx| {
buffer.edit([(0..5, "goodbye")], None, cx)
});
buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx));
assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
executor.run_until_parked();
// Client B joins the channel buffer
let channel_buffer_b = client_b
.channel_store()
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
channel_buffer_b.read_with(cx_b, |buffer, _| {
assert_collaborators(
buffer.collaborators(),
&[client_a.user_id(), client_b.user_id()],
);
});
// Client B sees the correct text, and then edits it
let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer());
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()),
buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id())
);
assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world");
buffer_b.update(cx_b, |buffer, cx| {
buffer.edit([(7..12, "beautiful")], None, cx)
});
// Both A and B see the new edit
executor.run_until_parked();
assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world");
assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world");
// Client A closes the channel buffer.
cx_a.update(|_| drop(channel_buffer_a));
executor.run_until_parked();
// Client B sees that client A is gone from the channel buffer.
channel_buffer_b.read_with(cx_b, |buffer, _| {
assert_collaborators(buffer.collaborators(), &[client_b.user_id()]);
});
// Client A rejoins the channel buffer
let _channel_buffer_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
executor.run_until_parked();
// Sanity test, make sure we saw A rejoining
channel_buffer_b.read_with(cx_b, |buffer, _| {
assert_collaborators(
buffer.collaborators(),
&[client_a.user_id(), client_b.user_id()],
);
});
// Client A loses connection.
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
// Client B observes A disconnect
channel_buffer_b.read_with(cx_b, |buffer, _| {
assert_collaborators(buffer.collaborators(), &[client_b.user_id()]);
});
// TODO:
// - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects
// - Test interaction with channel deletion while buffer is open
}
#[gpui::test]
async fn test_channel_notes_participant_indices(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
cx_c.update(editor::init);
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
client_a
.fs()
.insert_tree("/root", json!({"file.txt": "123"}))
.await;
let (project_a, worktree_id_a) = client_a.build_local_project_with_trust("/root", cx_a).await;
let project_b = client_b.build_empty_local_project(false, cx_b);
let project_c = client_c.build_empty_local_project(false, cx_c);
let (workspace_a, mut cx_a) = client_a.build_workspace(&project_a, cx_a);
let (workspace_b, mut cx_b) = client_b.build_workspace(&project_b, cx_b);
let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
// Clients A, B, and C open the channel notes
let channel_view_a = cx_a
.update(|window, cx| ChannelView::open(channel_id, None, workspace_a.clone(), window, cx))
.await
.unwrap();
let channel_view_b = cx_b
.update(|window, cx| ChannelView::open(channel_id, None, workspace_b.clone(), window, cx))
.await
.unwrap();
let channel_view_c = cx_c
.update(|window, cx| ChannelView::open(channel_id, None, workspace_c.clone(), window, cx))
.await
.unwrap();
// Clients A, B, and C all insert and select some text
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.insert("a", window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![MultiBufferOffset(0)..MultiBufferOffset(1)]);
});
});
});
executor.run_until_parked();
channel_view_b.update_in(cx_b, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("b", window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![MultiBufferOffset(1)..MultiBufferOffset(2)]);
});
});
});
executor.run_until_parked();
channel_view_c.update_in(cx_c, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("c", window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![MultiBufferOffset(2)..MultiBufferOffset(3)]);
});
});
});
// Client A sees clients B and C without assigned colors, because they aren't
// in a call together.
executor.run_until_parked();
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], window, cx);
});
});
// Clients A and B join the same call.
for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
}
// Clients A and B see each other with two different assigned colors. Client C
// still doesn't have a color.
executor.run_until_parked();
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
window,
cx,
);
});
});
channel_view_b.update_in(cx_b, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
window,
cx,
);
});
});
// Client A shares a project, and client B joins.
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
// Clients A and B open the same file.
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path(
(worktree_id_a, rel_path("file.txt")),
None,
true,
window,
cx,
)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path(
(worktree_id_a, rel_path("file.txt")),
None,
true,
window,
cx,
)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![MultiBufferOffset(0)..MultiBufferOffset(1)]);
});
});
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![MultiBufferOffset(2)..MultiBufferOffset(3)]);
});
});
executor.run_until_parked();
// Clients A and B see each other with the same colors as in the channel notes.
editor_a.update_in(cx_a, |editor, window, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], window, cx);
});
editor_b.update_in(cx_b, |editor, window, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], window, cx);
});
}
#[track_caller]
fn assert_remote_selections(
editor: &mut Editor,
expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
window: &mut Window,
cx: &mut Context<Editor>,
) {
let snapshot = editor.snapshot(window, cx);
let hub = editor.collaboration_hub().unwrap();
let collaborators = hub.collaborators(cx);
let range = Anchor::Min..Anchor::Max;
let remote_selections = snapshot
.remote_selections_in_range(&range, hub, cx)
.map(|s| {
let CollaboratorId::PeerId(peer_id) = s.collaborator_id else {
panic!("unexpected collaborator id");
};
let start = s.selection.start.to_offset(snapshot.buffer_snapshot());
let end = s.selection.end.to_offset(snapshot.buffer_snapshot());
let user_id = collaborators.get(&peer_id).unwrap().user_id;
let participant_index = hub.user_participant_indices(cx).get(&user_id).copied();
(participant_index, start.0..end.0)
})
.collect::<Vec<_>>();
assert_eq!(
remote_selections, expected_selections,
"incorrect remote selections"
);
}
#[gpui::test]
async fn test_multiple_handles_to_channel_buffer(
deterministic: BackgroundExecutor,
cx_a: &mut TestAppContext,
) {
let mut server = TestServer::start(deterministic.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let channel_id = server
.make_channel("the-channel", None, (&client_a, cx_a), &mut [])
.await;
let channel_buffer_1 = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
let channel_buffer_2 = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
let channel_buffer_3 = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
// All concurrent tasks for opening a channel buffer return the same model handle.
let (channel_buffer, channel_buffer_2, channel_buffer_3) =
future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
.await
.unwrap();
let channel_buffer_entity_id = channel_buffer.entity_id();
assert_eq!(channel_buffer, channel_buffer_2);
assert_eq!(channel_buffer, channel_buffer_3);
channel_buffer.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "hello")], None, cx);
})
});
deterministic.run_until_parked();
cx_a.update(|_| {
drop(channel_buffer);
drop(channel_buffer_2);
drop(channel_buffer_3);
});
deterministic.run_until_parked();
// The channel buffer can be reopened after dropping it.
let channel_buffer = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
assert_ne!(channel_buffer.entity_id(), channel_buffer_entity_id);
channel_buffer.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, _| {
assert_eq!(buffer.text(), "hello");
})
});
}
#[gpui::test]
async fn test_channel_buffer_disconnect(
deterministic: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(deterministic.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b)],
)
.await;
let channel_buffer_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
let channel_buffer_b = client_b
.channel_store()
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
channel_buffer_a.update(cx_a, |buffer, cx| {
assert_eq!(buffer.channel(cx).unwrap().name, "the-channel");
assert!(!buffer.is_connected());
});
deterministic.run_until_parked();
server.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
deterministic.run_until_parked();
client_a
.channel_store()
.update(cx_a, |channel_store, _| {
channel_store.remove_channel(channel_id)
})
.await
.unwrap();
deterministic.run_until_parked();
// Channel buffer observed the deletion
channel_buffer_b.update(cx_b, |buffer, cx| {
assert!(buffer.channel(cx).is_none());
assert!(!buffer.is_connected());
});
}
#[gpui::test]
async fn test_rejoin_channel_buffer(
deterministic: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(deterministic.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b)],
)
.await;
let channel_buffer_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
let channel_buffer_b = client_b
.channel_store()
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "1")], None, cx);
})
});
deterministic.run_until_parked();
// Client A disconnects.
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
// Both clients make an edit.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(1..1, "2")], None, cx);
})
});
channel_buffer_b.update(cx_b, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "0")], None, cx);
})
});
// Both clients see their own edit.
deterministic.run_until_parked();
channel_buffer_a.read_with(cx_a, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "12");
});
channel_buffer_b.read_with(cx_b, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "01");
});
// Client A reconnects. Both clients see each other's edits, and see
// the same collaborators.
server.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT);
channel_buffer_a.read_with(cx_a, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "012");
});
channel_buffer_b.read_with(cx_b, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "012");
});
channel_buffer_a.read_with(cx_a, |buffer_a, _| {
channel_buffer_b.read_with(cx_b, |buffer_b, _| {
assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
});
});
}
#[gpui::test]
async fn test_channel_buffers_and_server_restarts(
deterministic: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
let mut server = TestServer::start(deterministic.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
let channel_buffer_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
let channel_buffer_b = client_b
.channel_store()
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
let _channel_buffer_c = client_c
.channel_store()
.update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "1")], None, cx);
})
});
deterministic.run_until_parked();
// Client C can't reconnect.
client_c.override_establish_connection(|_, cx| cx.spawn(async |_| future::pending().await));
// Server stops.
server.reset().await;
deterministic.advance_clock(RECEIVE_TIMEOUT);
// While the server is down, both clients make an edit.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(1..1, "2")], None, cx);
})
});
channel_buffer_b.update(cx_b, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "0")], None, cx);
})
});
// Server restarts.
server.start().await.unwrap();
deterministic.advance_clock(CLEANUP_TIMEOUT);
// Clients reconnects. Clients A and B see each other's edits, and see
// that client C has disconnected.
channel_buffer_a.read_with(cx_a, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "012");
});
channel_buffer_b.read_with(cx_b, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "012");
});
channel_buffer_a.read_with(cx_a, |buffer_a, _| {
channel_buffer_b.read_with(cx_b, |buffer_b, _| {
assert_collaborators(
buffer_a.collaborators(),
&[client_a.user_id(), client_b.user_id()],
);
assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
});
});
}
#[gpui::test]
async fn test_channel_buffer_changes(
deterministic: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let (server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
let (_, cx_a) = client_a.build_test_workspace(cx_a).await;
let (workspace_b, cx_b) = client_b.build_test_workspace(cx_b).await;
let channel_store_b = client_b.channel_store().clone();
// Editing the channel notes should set them to dirty
open_channel_notes(channel_id, cx_a).await.unwrap();
cx_a.simulate_keystrokes("1");
channel_store_b.read_with(cx_b, |channel_store, _| {
assert!(channel_store.has_channel_buffer_changed(channel_id))
});
// Opening the buffer should clear the changed flag.
open_channel_notes(channel_id, cx_b).await.unwrap();
channel_store_b.read_with(cx_b, |channel_store, _| {
assert!(!channel_store.has_channel_buffer_changed(channel_id))
});
// Editing the channel while the buffer is open should not show that the buffer has changed.
cx_a.simulate_keystrokes("2");
channel_store_b.read_with(cx_b, |channel_store, _| {
assert!(!channel_store.has_channel_buffer_changed(channel_id))
});
// Test that the server is tracking things correctly, and we retain our 'not changed'
// state across a disconnect
deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL);
server
.simulate_long_connection_interruption(client_b.peer_id().unwrap(), deterministic.clone());
// Re-subscribe to channels after reconnection (simulates collab panel re-rendering)
client_b.initialize_channel_store(cx_b);
deterministic.run_until_parked();
channel_store_b.read_with(cx_b, |channel_store, _| {
assert!(!channel_store.has_channel_buffer_changed(channel_id))
});
// Closing the buffer should re-enable change tracking
cx_b.update(|window, cx| {
workspace_b.update(cx, |workspace, cx| {
workspace.close_all_items_and_panes(&Default::default(), window, cx)
});
});
deterministic.run_until_parked();
cx_a.simulate_keystrokes("3");
channel_store_b.read_with(cx_b, |channel_store, _| {
assert!(channel_store.has_channel_buffer_changed(channel_id))
});
}
#[gpui::test]
async fn test_channel_buffer_changes_persist(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_b2: &mut TestAppContext,
) {
let (mut server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
let (_, cx_a) = client_a.build_test_workspace(cx_a).await;
let (_, cx_b) = client_b.build_test_workspace(cx_b).await;
// a) edits the notes
open_channel_notes(channel_id, cx_a).await.unwrap();
cx_a.simulate_keystrokes("1");
// b) opens them to observe the current version
open_channel_notes(channel_id, cx_b).await.unwrap();
// On boot the client should get the correct state.
let client_b2 = server.create_client(cx_b2, "user_b").await;
let channel_store_b2 = client_b2.channel_store().clone();
channel_store_b2.read_with(cx_b2, |channel_store, _| {
assert!(!channel_store.has_channel_buffer_changed(channel_id))
});
}
#[gpui::test]
async fn test_channel_buffer_operations_lost_on_reconnect(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b)],
)
.await;
// Both clients open the channel buffer.
let channel_buffer_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
let channel_buffer_b = client_b
.channel_store()
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
// Step 1: Client A makes an initial edit that syncs to B.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "a")], None, cx);
})
});
executor.run_until_parked();
// Verify both clients see "a".
channel_buffer_a.read_with(cx_a, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "a");
});
channel_buffer_b.read_with(cx_b, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "a");
});
// Step 2: Disconnect client A. Do NOT advance past RECONNECT_TIMEOUT
// so that the buffer stays in `opened_buffers` for rejoin.
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
executor.run_until_parked();
// Step 3: While disconnected, client A makes an offline edit ("b").
// on_buffer_update fires but client.send() fails because transport is down.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(1..1, "b")], None, cx);
})
});
executor.run_until_parked();
// Client A sees "ab" locally; B still sees "a".
channel_buffer_a.read_with(cx_a, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "ab");
});
channel_buffer_b.read_with(cx_b, |buffer, cx| {
assert_eq!(buffer.buffer().read(cx).text(), "a");
});
// Step 4: Reconnect and make a racing edit in parallel.
//
// The race condition occurs when:
// 1. Transport reconnects, handle_connect captures version V (with "b") and sends RejoinChannelBuffers
// 2. DURING the async gap (awaiting response), user makes edit "c"
// 3. on_buffer_update sends UpdateChannelBuffer (succeeds because transport is up)
// 4. Server receives BOTH messages concurrently (FuturesUnordered)
// 5. If UpdateChannelBuffer commits first, server version is inflated to include "c"
// 6. RejoinChannelBuffers reads inflated version and sends it back
// 7. Client's serialize_ops(inflated_version) filters out "b" (offline edit)
// because the inflated version's timestamp covers "b"'s timestamp
// Get the buffer handle for spawning
let buffer_for_edit = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
// Spawn the edit task - it will wait for executor to run it
let edit_task = cx_a.spawn({
let buffer = buffer_for_edit;
async move |mut cx| {
let _ = buffer.update(&mut cx, |buffer, cx| {
buffer.edit([(2..2, "c")], None, cx);
});
}
});
// Allow connections so reconnect can succeed
server.allow_connections();
// Advance clock to trigger reconnection attempt
executor.advance_clock(RECEIVE_TIMEOUT);
// Run the edit task - this races with handle_connect
edit_task.detach();
// Let everything settle.
executor.run_until_parked();
// Step 7: Read final buffer text from both clients.
let text_a = channel_buffer_a.read_with(cx_a, |buffer, cx| buffer.buffer().read(cx).text());
let text_b = channel_buffer_b.read_with(cx_b, |buffer, cx| buffer.buffer().read(cx).text());
// Both clients must see the same text containing all three edits.
assert_eq!(
text_a, text_b,
"Client A and B diverged! A sees {:?}, B sees {:?}. \
Operations were lost during reconnection.",
text_a, text_b
);
assert!(
text_a.contains('a'),
"Initial edit 'a' missing from final text {:?}",
text_a
);
assert!(
text_a.contains('b'),
"Offline edit 'b' missing from final text {:?}. \
This is the reconnection race bug: the offline operation was \
filtered out by serialize_ops because the server_version was \
inflated by a racing UpdateChannelBuffer.",
text_a
);
assert!(
text_a.contains('c'),
"Racing edit 'c' missing from final text {:?}",
text_a
);
// Step 8: Verify the invariant directly — every operation known to
// client A must be observed by client B's version. If any operation
// in A's history is not covered by B's version, it was lost.
channel_buffer_a.read_with(cx_a, |buf_a, cx_a_inner| {
let buffer_a = buf_a.buffer().read(cx_a_inner);
let ops_a = buffer_a.operations();
channel_buffer_b.read_with(cx_b, |buf_b, cx_b_inner| {
let buffer_b = buf_b.buffer().read(cx_b_inner);
let version_b = buffer_b.version();
for (lamport, _op) in ops_a.iter() {
assert!(
version_b.observed(*lamport),
"Operation with lamport timestamp {:?} from client A \
is NOT observed by client B's version. This operation \
was lost during reconnection.",
lamport
);
}
});
});
}
#[track_caller]
fn assert_collaborators(
collaborators: &HashMap<PeerId, Collaborator>,
ids: &[Option<LegacyUserId>],
) {
let mut user_ids = collaborators
.values()
.map(|collaborator| collaborator.user_id)
.collect::<Vec<_>>();
user_ids.sort();
assert_eq!(
user_ids,
ids.iter().map(|id| id.unwrap()).collect::<Vec<_>>()
);
}
fn buffer_text(channel_buffer: &Entity<language::Buffer>, cx: &mut TestAppContext) -> String {
channel_buffer.read_with(cx, |buffer, _| buffer.text())
}

View File

@@ -0,0 +1,314 @@
use crate::TestServer;
use call::ActiveCall;
use collab::db::ChannelId;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext};
use rpc::proto;
use util::rel_path::rel_path;
#[gpui::test]
async fn test_channel_guests(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let active_call_a = cx_a.read(ActiveCall::global);
let channel_id = server
.make_public_channel("the-channel", &client_a, cx_a)
.await;
// Client A shares a project in the channel
let project_a = client_a.build_test_project(cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
cx_a.executor().run_until_parked();
// Client B joins channel A as a guest
cx_b.update(|cx| {
workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx)
})
.await
.unwrap();
// b should be following a in the shared project.
// B is a guest,
executor.run_until_parked();
let active_call_b = cx_b.read(ActiveCall::global);
let project_b =
active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap());
let room_b = active_call_b.update(cx_b, |call, _| call.room().unwrap().clone());
assert_eq!(
project_b.read_with(cx_b, |project, _| project.remote_id()),
Some(project_id),
);
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(
project_b
.update(cx_b, |project, cx| {
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
project.create_entry((worktree_id, rel_path("b.txt")), false, cx)
})
.await
.is_err()
);
assert!(room_b.read_with(cx_b, |room, _| room.is_muted()));
}
#[gpui::test]
async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let active_call_a = cx_a.read(ActiveCall::global);
let channel_id = server
.make_public_channel("the-channel", &client_a, cx_a)
.await;
let project_a = client_a.build_test_project(cx_a).await;
cx_a.update(|cx| {
workspace::join_channel(channel_id, client_a.app_state.clone(), None, None, cx)
})
.await
.unwrap();
// Client A shares a project in the channel
active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
cx_a.run_until_parked();
// Client B joins channel A as a guest
cx_b.update(|cx| {
workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx)
})
.await
.unwrap();
cx_a.run_until_parked();
// client B opens 1.txt as a guest
let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
let room_b = cx_b
.read(ActiveCall::global)
.update(cx_b, |call, _| call.room().unwrap().clone());
cx_b.simulate_keystrokes("cmd-p");
cx_a.run_until_parked();
cx_b.simulate_keystrokes("1 enter");
let (project_b, editor_b) = workspace_b.update(cx_b, |workspace, cx| {
(
workspace.project().clone(),
workspace.active_item_as::<Editor>(cx).unwrap(),
)
});
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
cx_b.update(|_window, cx_b| {
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
});
assert!(
room_b
.update(cx_b, |room, cx| room.share_microphone(cx))
.await
.is_err()
);
// B is promoted
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
)
})
})
.await
.unwrap();
cx_a.run_until_parked();
// project and buffers are now editable
assert!(project_b.read_with(cx_b, |project, cx| !project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
// B sees themselves as muted, and can unmute.
cx_b.update(|_window, cx_b| {
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
});
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
cx_a.run_until_parked();
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
// B is demoted
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Guest,
cx,
)
})
})
.await
.unwrap();
cx_a.run_until_parked();
// project and buffers are no longer editable
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx)));
assert!(
room_b
.update(cx_b, |room, cx| room.share_microphone(cx))
.await
.is_err()
);
}
#[gpui::test]
async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
// Create a parent channel that requires the Zed CLA
let parent_channel_id = server
.make_channel("the-channel", None, (&client_a, cx_a), &mut [])
.await;
server
.app_state
.db
.set_channel_requires_zed_cla(ChannelId::from_proto(parent_channel_id.0), true)
.await
.unwrap();
// Create a public channel that is a child of the parent channel.
let channel_id = client_a
.channel_store()
.update(cx_a, |store, cx| {
store.create_channel("the-sub-channel", Some(parent_channel_id), cx)
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |store, cx| {
store.set_channel_visibility(parent_channel_id, proto::ChannelVisibility::Public, cx)
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |store, cx| {
store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx)
})
.await
.unwrap();
// Users A and B join the channel. B is a guest.
active_call_a
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
cx_a.run_until_parked();
let room_b = cx_b
.read(ActiveCall::global)
.update(cx_b, |call, _| call.room().unwrap().clone());
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
});
// A tries to grant write access to B, but cannot because B has not
// yet signed the zed CLA.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
)
})
})
.await
.unwrap_err();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
});
// A tries to grant write access to B, but cannot because B has not
// yet signed the zed CLA.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Talker,
cx,
)
})
})
.await
.unwrap();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
});
// User B signs the zed CLA.
let user_b = server
.app_state
.user_service
.get_user_by_github_login("user_b")
.await
.unwrap()
.expect("user_b not found");
server
.app_state
.db
.add_contributor(user_b.id)
.await
.unwrap();
// A can now grant write access to B.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
)
})
})
.await
.unwrap();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,356 @@
use crate::TestServer;
use collab_ui::CollabPanel;
use collab_ui::collab_panel::{MoveChannelDown, MoveChannelUp, ToggleSelectedChannelFavorite};
use gpui::TestAppContext;
use menu::{SelectNext, SelectPrevious};
#[gpui::test]
async fn test_reorder_favorite_channels_independently_of_channels(cx: &mut TestAppContext) {
let (server, client) = TestServer::start1(cx).await;
let root = server
.make_channel("root", None, (&client, cx), &mut [])
.await;
let _ = server
.make_channel("channel-a", Some(root), (&client, cx), &mut [])
.await;
let _ = server
.make_channel("channel-b", Some(root), (&client, cx), &mut [])
.await;
let _ = server
.make_channel("channel-c", Some(root), (&client, cx), &mut [])
.await;
let (workspace, cx) = client.build_test_workspace(cx).await;
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = CollabPanel::new(workspace, window, cx);
workspace.add_panel(panel.clone(), window, cx);
panel
});
cx.run_until_parked();
// Verify initial state.
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Select channel-b.
panel.update_in(cx, |panel, window, cx| {
panel.select_next(&SelectNext, window, cx);
panel.select_next(&SelectNext, window, cx);
panel.select_next(&SelectNext, window, cx);
panel.select_next(&SelectNext, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b <== selected",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Favorite channel-b.
panel.update_in(cx, |panel, window, cx| {
panel.toggle_selected_channel_favorite(&ToggleSelectedChannelFavorite, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-b",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b <== selected",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Select channel-c.
panel.update_in(cx, |panel, window, cx| {
panel.select_next(&SelectNext, window, cx);
});
// Favorite channel-c.
panel.update_in(cx, |panel, window, cx| {
panel.toggle_selected_channel_favorite(&ToggleSelectedChannelFavorite, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c <== selected",
"[Contacts]",
]
);
// Navigate up to favorite channel-b .
panel.update_in(cx, |panel, window, cx| {
panel.select_previous(&SelectPrevious, window, cx);
panel.select_previous(&SelectPrevious, window, cx);
panel.select_previous(&SelectPrevious, window, cx);
panel.select_previous(&SelectPrevious, window, cx);
panel.select_previous(&SelectPrevious, window, cx);
panel.select_previous(&SelectPrevious, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-b <== selected",
" #️⃣ channel-c",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Move favorite channel-b down.
// The Channels section should remain unchanged
panel.update_in(cx, |panel, window, cx| {
panel.move_channel_down(&MoveChannelDown, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-c",
" #️⃣ channel-b <== selected",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Move favorite channel-b down again when it's already last (should be no-op).
panel.update_in(cx, |panel, window, cx| {
panel.move_channel_down(&MoveChannelDown, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-c",
" #️⃣ channel-b <== selected",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Move favorite channel-b back up.
// The Channels section should remain unchanged.
panel.update_in(cx, |panel, window, cx| {
panel.move_channel_up(&MoveChannelUp, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-b <== selected",
" #️⃣ channel-c",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Move favorite channel-b up again when it's already first (should be no-op).
panel.update_in(cx, |panel, window, cx| {
panel.move_channel_up(&MoveChannelUp, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-b <== selected",
" #️⃣ channel-c",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Unfavorite channel-b.
// Selection should move to the next favorite (channel-c).
panel.update_in(cx, |panel, window, cx| {
panel.toggle_selected_channel_favorite(&ToggleSelectedChannelFavorite, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-c <== selected",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Unfavorite channel-c.
// Favorites section should disappear entirely.
// Selection should move to the next available item.
panel.update_in(cx, |panel, window, cx| {
panel.toggle_selected_channel_favorite(&ToggleSelectedChannelFavorite, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Channels]",
" v root <== selected",
" #️⃣ channel-a",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
}
#[gpui::test]
async fn test_reorder_channels_independently_of_favorites(cx: &mut TestAppContext) {
let (server, client) = TestServer::start1(cx).await;
let root = server
.make_channel("root", None, (&client, cx), &mut [])
.await;
let _ = server
.make_channel("channel-a", Some(root), (&client, cx), &mut [])
.await;
let _ = server
.make_channel("channel-b", Some(root), (&client, cx), &mut [])
.await;
let _ = server
.make_channel("channel-c", Some(root), (&client, cx), &mut [])
.await;
let (workspace, cx) = client.build_test_workspace(cx).await;
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = CollabPanel::new(workspace, window, cx);
workspace.add_panel(panel.clone(), window, cx);
panel
});
cx.run_until_parked();
// Select channel-a.
panel.update_in(cx, |panel, window, cx| {
panel.select_next(&SelectNext, window, cx);
panel.select_next(&SelectNext, window, cx);
panel.select_next(&SelectNext, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Channels]",
" v root",
" #️⃣ channel-a <== selected",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Favorite channel-a.
panel.update_in(cx, |panel, window, cx| {
panel.toggle_selected_channel_favorite(&ToggleSelectedChannelFavorite, window, cx);
});
// Select channel-b.
// Favorite channel-b.
panel.update_in(cx, |panel, window, cx| {
panel.select_next(&SelectNext, window, cx);
panel.toggle_selected_channel_favorite(&ToggleSelectedChannelFavorite, window, cx);
});
cx.run_until_parked();
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-a",
" #️⃣ channel-b",
"[Channels]",
" v root",
" #️⃣ channel-a",
" #️⃣ channel-b <== selected",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Select channel-a in the Channels section.
panel.update_in(cx, |panel, window, cx| {
panel.select_previous(&SelectPrevious, window, cx);
});
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-a",
" #️⃣ channel-b",
"[Channels]",
" v root",
" #️⃣ channel-a <== selected",
" #️⃣ channel-b",
" #️⃣ channel-c",
"[Contacts]",
]
);
// Move channel-a down.
// The Favorites section should remain unchanged.
// Selection should remain on channel-a in the Channels section,
// not jump to channel-a in Favorites.
panel.update_in(cx, |panel, window, cx| {
panel.move_channel_down(&MoveChannelDown, window, cx);
});
cx.run_until_parked();
assert_eq!(
panel.read_with(cx, |panel, _| panel.entries_as_strings()),
&[
"[Favorites]",
" #️⃣ channel-a",
" #️⃣ channel-b",
"[Channels]",
" v root",
" #️⃣ channel-b",
" #️⃣ channel-a <== selected",
" #️⃣ channel-c",
"[Contacts]",
]
);
}

View File

@@ -0,0 +1,54 @@
use call::Room;
use client::ChannelId;
use gpui::{Entity, TestAppContext};
mod agent_sharing_tests;
mod auto_watch_tests;
mod channel_buffer_tests;
mod channel_guest_tests;
mod channel_tests;
mod collab_panel_tests;
mod db_tests;
mod editor_tests;
mod following_tests;
mod git_tests;
mod integration_tests;
mod notification_tests;
mod random_channel_buffer_tests;
mod random_project_collaboration_tests;
mod randomized_test_helpers;
mod remote_editing_collaboration_tests;
mod test_server;
pub use randomized_test_helpers::{
RandomizedTest, TestError, UserTestPlan, run_randomized_test, save_randomized_test_plan,
};
pub use test_server::{TestClient, TestServer};
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
pending: Vec<String>,
}
fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomParticipants {
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.values()
.map(|participant| participant.user.github_login.clone().to_string())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()
.iter()
.map(|user| user.github_login.clone().to_string())
.collect::<Vec<_>>();
remote.sort();
pending.sort();
RoomParticipants { remote, pending }
})
}
fn channel_id(room: &Entity<Room>, cx: &mut TestAppContext) -> Option<ChannelId> {
cx.read(|cx| room.read(cx).channel_id())
}

View File

@@ -0,0 +1,223 @@
mod buffer_tests;
mod channel_tests;
mod db_tests;
mod extension_tests;
mod migrations;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering::SeqCst};
use std::time::Duration;
use collections::HashSet;
use gpui::BackgroundExecutor;
use parking_lot::Mutex;
use rand::prelude::*;
use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
use self::migrations::run_database_migrations;
use collab::db::*;
pub struct TestDb {
pub db: Option<Arc<Database>>,
pub connection: Option<sqlx::AnyConnection>,
}
impl TestDb {
pub fn sqlite(executor: BackgroundExecutor) -> Self {
let url = "sqlite::memory:";
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap();
let mut db = runtime.block_on(async {
let mut options = ConnectOptions::new(url);
options.max_connections(5);
let mut db = Database::new(options).await.unwrap();
let sql = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/migrations.sqlite/20221109000000_test_schema.sql"
));
db.pool
.execute(sea_orm::Statement::from_string(
db.pool.get_database_backend(),
sql,
))
.await
.unwrap();
db.initialize_notification_kinds().await.unwrap();
db
});
db.test_options = Some(DatabaseTestOptions {
executor,
runtime,
query_failure_probability: parking_lot::Mutex::new(0.0),
});
Self {
db: Some(Arc::new(db)),
connection: None,
}
}
pub fn postgres(executor: BackgroundExecutor) -> Self {
static LOCK: Mutex<()> = Mutex::new(());
let _guard = LOCK.lock();
let mut rng = StdRng::from_os_rng();
let url = format!(
"postgres://postgres@localhost/zed-test-{}",
rng.random::<u128>()
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap();
let mut db = runtime.block_on(async {
sqlx::Postgres::create_database(&url)
.await
.expect("failed to create test db");
let mut options = ConnectOptions::new(url);
options
.max_connections(5)
.idle_timeout(Duration::from_secs(0));
let mut db = Database::new(options).await.unwrap();
let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
run_database_migrations(db.options(), migrations_path)
.await
.unwrap();
db.initialize_notification_kinds().await.unwrap();
db
});
db.test_options = Some(DatabaseTestOptions {
executor,
runtime,
query_failure_probability: parking_lot::Mutex::new(0.0),
});
Self {
db: Some(Arc::new(db)),
connection: None,
}
}
pub fn db(&self) -> &Arc<Database> {
self.db.as_ref().unwrap()
}
pub fn set_query_failure_probability(&self, probability: f64) {
let database = self.db.as_ref().unwrap();
let test_options = database.test_options.as_ref().unwrap();
*test_options.query_failure_probability.lock() = probability;
}
}
#[macro_export]
macro_rules! test_both_dbs {
($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
#[gpui::test]
async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
// In CI, only run postgres tests on Linux (where we have the postgres service).
// Locally, always run them (assuming postgres is available).
if std::env::var("CI").is_ok() && !cfg!(target_os = "linux") {
return;
}
let test_db = $crate::db_tests::TestDb::postgres(cx.executor().clone());
$test_name(test_db.db()).await;
}
#[gpui::test]
async fn $sqlite_test_name(cx: &mut gpui::TestAppContext) {
let test_db = $crate::db_tests::TestDb::sqlite(cx.executor().clone());
$test_name(test_db.db()).await;
}
};
}
impl Drop for TestDb {
fn drop(&mut self) {
let db = self.db.take().unwrap();
if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() {
db.test_options.as_ref().unwrap().runtime.block_on(async {
use util::ResultExt;
let query = "
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE
pg_stat_activity.datname = current_database() AND
pid <> pg_backend_pid();
";
db.pool
.execute(sea_orm::Statement::from_string(
db.pool.get_database_backend(),
query,
))
.await
.log_err();
sqlx::Postgres::drop_database(db.options.get_url())
.await
.log_err();
})
}
}
}
#[track_caller]
fn assert_channel_tree_matches(actual: Vec<Channel>, expected: Vec<Channel>) {
let expected_channels = expected.into_iter().collect::<HashSet<_>>();
let actual_channels = actual.into_iter().collect::<HashSet<_>>();
pretty_assertions::assert_eq!(expected_channels, actual_channels);
}
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
use std::collections::HashMap;
let mut result = Vec::new();
let mut order_by_parent: HashMap<Vec<ChannelId>, i32> = HashMap::new();
for (id, parent_path, name) in channels {
let parent_key = parent_path.to_vec();
let order = if parent_key.is_empty() {
1
} else {
*order_by_parent
.entry(parent_key.clone())
.and_modify(|e| *e += 1)
.or_insert(1)
};
result.push(Channel {
id: *id,
name: (*name).to_owned(),
visibility: ChannelVisibility::Members,
parent_path: parent_key,
channel_order: order,
});
}
result
}
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
db.create_user(
email,
None,
false,
NewUserParams {
github_login: email[0..email.find('@').unwrap()].to_string(),
github_user_id: GITHUB_USER_ID.fetch_add(1, SeqCst),
},
)
.await
.unwrap()
.user_id
}

View File

@@ -0,0 +1,362 @@
use super::*;
use crate::test_both_dbs;
use language::proto::{self, serialize_version};
use rpc::ConnectionId;
use text::{Buffer, ReplicaId};
test_both_dbs!(
test_channel_buffers,
test_channel_buffers_postgres,
test_channel_buffers_sqlite
);
async fn test_channel_buffers(db: &Arc<Database>) {
let a_id = db
.create_user(
"user_a@example.com",
None,
false,
NewUserParams {
github_login: "user_a".into(),
github_user_id: 101,
},
)
.await
.unwrap()
.user_id;
let b_id = db
.create_user(
"user_b@example.com",
None,
false,
NewUserParams {
github_login: "user_b".into(),
github_user_id: 102,
},
)
.await
.unwrap()
.user_id;
// This user will not be a part of the channel
let c_id = db
.create_user(
"user_c@example.com",
None,
false,
NewUserParams {
github_login: "user_c".into(),
github_user_id: 103,
},
)
.await
.unwrap()
.user_id;
let owner_id = db.create_server("production").await.unwrap().0 as u32;
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
.await
.unwrap();
db.respond_to_channel_invite(zed_id, b_id, true)
.await
.unwrap();
let connection_id_a = ConnectionId { owner_id, id: 1 };
let _ = db
.join_channel_buffer(zed_id, a_id, connection_id_a)
.await
.unwrap();
let mut buffer_a = Buffer::new(
ReplicaId::new(0),
text::BufferId::new(1).unwrap(),
"".to_string(),
);
let operations = vec![
buffer_a.edit([(0..0, "hello world")]),
buffer_a.edit([(5..5, ", cruel")]),
buffer_a.edit([(0..5, "goodbye")]),
buffer_a.undo().unwrap().1,
];
assert_eq!(buffer_a.text(), "hello, cruel world");
let operations = operations
.into_iter()
.map(|op| proto::serialize_operation(&language::Operation::Buffer(op)))
.collect::<Vec<_>>();
db.update_channel_buffer(zed_id, a_id, &operations)
.await
.unwrap();
let connection_id_b = ConnectionId { owner_id, id: 2 };
let buffer_response_b = db
.join_channel_buffer(zed_id, b_id, connection_id_b)
.await
.unwrap();
let mut buffer_b = Buffer::new(
ReplicaId::new(0),
text::BufferId::new(1).unwrap(),
buffer_response_b.base_text,
);
buffer_b.apply_ops(buffer_response_b.operations.into_iter().map(|operation| {
let operation = proto::deserialize_operation(operation).unwrap();
if let language::Operation::Buffer(operation) = operation {
operation
} else {
unreachable!()
}
}));
assert_eq!(buffer_b.text(), "hello, cruel world");
// Ensure that C fails to open the buffer
assert!(
db.join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 })
.await
.is_err()
);
// Ensure that both collaborators have shown up
assert_eq!(
buffer_response_b.collaborators,
&[
rpc::proto::Collaborator {
user_id: a_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
replica_id: ReplicaId::FIRST_COLLAB_ID.as_u16() as u32,
is_host: false,
committer_name: None,
committer_email: None,
},
rpc::proto::Collaborator {
user_id: b_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
replica_id: ReplicaId::FIRST_COLLAB_ID.as_u16() as u32 + 1,
is_host: false,
committer_name: None,
committer_email: None,
}
]
);
// Ensure that get_channel_buffer_collaborators works
let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
assert_eq!(zed_collaborats, &[a_id, b_id]);
let left_buffer = db
.leave_channel_buffer(zed_id, connection_id_b)
.await
.unwrap();
assert_eq!(left_buffer.connections, &[connection_id_a],);
let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap();
let _ = db
.join_channel_buffer(cargo_id, a_id, connection_id_a)
.await
.unwrap();
db.leave_channel_buffers(connection_id_a).await.unwrap();
let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap();
assert_eq!(zed_collaborators, &[]);
assert_eq!(cargo_collaborators, &[]);
// When everyone has left the channel, the operations are collapsed into
// a new base text.
let buffer_response_b = db
.join_channel_buffer(zed_id, b_id, connection_id_b)
.await
.unwrap();
assert_eq!(buffer_response_b.base_text, "hello, cruel world");
assert_eq!(buffer_response_b.operations, &[]);
}
test_both_dbs!(
test_channel_buffers_last_operations,
test_channel_buffers_last_operations_postgres,
test_channel_buffers_last_operations_sqlite
);
async fn test_channel_buffers_last_operations(db: &Database) {
let user_id = db
.create_user(
"user_a@example.com",
None,
false,
NewUserParams {
github_login: "user_a".into(),
github_user_id: 101,
},
)
.await
.unwrap()
.user_id;
let observer_id = db
.create_user(
"user_b@example.com",
None,
false,
NewUserParams {
github_login: "user_b".into(),
github_user_id: 102,
},
)
.await
.unwrap()
.user_id;
let owner_id = db.create_server("production").await.unwrap().0 as u32;
let connection_id = ConnectionId {
owner_id,
id: user_id.0 as u32,
};
let mut buffers = Vec::new();
let mut text_buffers = Vec::new();
for i in 0..3 {
let channel = db
.create_root_channel(&format!("channel-{i}"), user_id)
.await
.unwrap();
db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member)
.await
.unwrap();
db.respond_to_channel_invite(channel, observer_id, true)
.await
.unwrap();
let res = db
.join_channel_buffer(channel, user_id, connection_id)
.await
.unwrap();
buffers.push(
db.transaction(|tx| async move { db.get_channel_buffer(channel, &tx).await })
.await
.unwrap(),
);
text_buffers.push(Buffer::new(
ReplicaId::new(res.replica_id as u16),
text::BufferId::new(1).unwrap(),
"".to_string(),
));
}
update_buffer(
buffers[0].channel_id,
user_id,
db,
vec![
text_buffers[0].edit([(0..0, "a")]),
text_buffers[0].edit([(0..0, "b")]),
text_buffers[0].edit([(0..0, "c")]),
],
)
.await;
update_buffer(
buffers[1].channel_id,
user_id,
db,
vec![
text_buffers[1].edit([(0..0, "d")]),
text_buffers[1].edit([(1..1, "e")]),
text_buffers[1].edit([(2..2, "f")]),
],
)
.await;
// cause buffer 1's epoch to increment.
db.leave_channel_buffer(buffers[1].channel_id, connection_id)
.await
.unwrap();
db.join_channel_buffer(buffers[1].channel_id, user_id, connection_id)
.await
.unwrap();
let replica_id = text_buffers[1].replica_id();
text_buffers[1] = Buffer::new(
replica_id,
text::BufferId::new(1).unwrap(),
"def".to_string(),
);
update_buffer(
buffers[1].channel_id,
user_id,
db,
vec![
text_buffers[1].edit([(0..0, "g")]),
text_buffers[1].edit([(0..0, "h")]),
],
)
.await;
update_buffer(
buffers[2].channel_id,
user_id,
db,
vec![text_buffers[2].edit([(0..0, "i")])],
)
.await;
let channels_for_user = db.get_channels_for_user(user_id).await.unwrap();
pretty_assertions::assert_eq!(
channels_for_user.latest_buffer_versions,
[
rpc::proto::ChannelBufferVersion {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version())
.into_iter()
.filter(
|vector| vector.replica_id == text_buffers[0].replica_id().as_u16() as u32
)
.collect::<Vec<_>>(),
},
rpc::proto::ChannelBufferVersion {
channel_id: buffers[1].channel_id.to_proto(),
epoch: 1,
version: serialize_version(&text_buffers[1].version())
.into_iter()
.filter(
|vector| vector.replica_id == text_buffers[1].replica_id().as_u16() as u32
)
.collect::<Vec<_>>(),
},
rpc::proto::ChannelBufferVersion {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version())
.into_iter()
.filter(
|vector| vector.replica_id == text_buffers[2].replica_id().as_u16() as u32
)
.collect::<Vec<_>>(),
},
]
);
}
async fn update_buffer(
channel_id: ChannelId,
user_id: UserId,
db: &Database,
operations: Vec<text::Operation>,
) {
let operations = operations
.into_iter()
.map(|op| proto::serialize_operation(&language::Operation::Buffer(op)))
.collect::<Vec<_>>();
db.update_channel_buffer(channel_id, user_id, &operations)
.await
.unwrap();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,383 @@
use crate::test_both_dbs;
use super::*;
use collab::db::RoomId;
use collab::db::*;
use pretty_assertions::assert_eq;
use rpc::ConnectionId;
use std::sync::Arc;
test_both_dbs!(
test_add_contacts,
test_add_contacts_postgres,
test_add_contacts_sqlite
);
async fn test_add_contacts(db: &Arc<Database>) {
let mut user_ids = Vec::new();
for i in 0..3 {
user_ids.push(
db.create_user(
&format!("user{i}@example.com"),
None,
false,
NewUserParams {
github_login: format!("user{i}"),
github_user_id: i,
},
)
.await
.unwrap()
.user_id,
);
}
let user_1 = user_ids[0];
let user_2 = user_ids[1];
let user_3 = user_ids[2];
// User starts with no contacts
assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
// User requests a contact. Both users see the pending request.
db.send_contact_request(user_1, user_2).await.unwrap();
assert!(!db.has_contact(user_1, user_2).await.unwrap());
assert!(!db.has_contact(user_2, user_1).await.unwrap());
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[Contact::Outgoing { user_id: user_2 }],
);
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[Contact::Incoming { user_id: user_1 }]
);
// User 2 dismisses the contact request notification without accepting or rejecting.
// We shouldn't notify them again.
db.dismiss_contact_notification(user_1, user_2)
.await
.unwrap_err();
db.dismiss_contact_notification(user_2, user_1)
.await
.unwrap();
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[Contact::Incoming { user_id: user_1 }]
);
// User can't accept their own contact request
db.respond_to_contact_request(user_1, user_2, true)
.await
.unwrap_err();
// User accepts a contact request. Both users see the contact.
db.respond_to_contact_request(user_2, user_1, true)
.await
.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[Contact::Accepted {
user_id: user_2,
busy: false,
}],
);
assert!(db.has_contact(user_1, user_2).await.unwrap());
assert!(db.has_contact(user_2, user_1).await.unwrap());
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[Contact::Accepted {
user_id: user_1,
busy: false,
}]
);
// Users cannot re-request existing contacts.
db.send_contact_request(user_1, user_2).await.unwrap_err();
db.send_contact_request(user_2, user_1).await.unwrap_err();
// Users can't dismiss notifications of them accepting other users' requests.
db.dismiss_contact_notification(user_2, user_1)
.await
.unwrap_err();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[Contact::Accepted {
user_id: user_2,
busy: false,
}]
);
// Users can dismiss notifications of other users accepting their requests.
db.dismiss_contact_notification(user_1, user_2)
.await
.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[Contact::Accepted {
user_id: user_2,
busy: false,
}]
);
// Users send each other concurrent contact requests and
// see that they are immediately accepted.
db.send_contact_request(user_1, user_3).await.unwrap();
db.send_contact_request(user_3, user_1).await.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_2,
busy: false,
},
Contact::Accepted {
user_id: user_3,
busy: false,
}
]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[Contact::Accepted {
user_id: user_1,
busy: false,
}],
);
// User declines a contact request. Both users see that it is gone.
db.send_contact_request(user_2, user_3).await.unwrap();
db.respond_to_contact_request(user_3, user_2, false)
.await
.unwrap();
assert!(!db.has_contact(user_2, user_3).await.unwrap());
assert!(!db.has_contact(user_3, user_2).await.unwrap());
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[Contact::Accepted {
user_id: user_1,
busy: false,
}]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[Contact::Accepted {
user_id: user_1,
busy: false,
}],
);
}
test_both_dbs!(
test_project_count,
test_project_count_postgres,
test_project_count_sqlite
);
async fn test_project_count(db: &Arc<Database>) {
let owner_id = db.create_server("test").await.unwrap().0 as u32;
let user1 = db
.create_user(
"admin@example.com",
None,
true,
NewUserParams {
github_login: "admin".into(),
github_user_id: 0,
},
)
.await
.unwrap();
let user2 = db
.create_user(
"user@example.com",
None,
false,
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
},
)
.await
.unwrap();
let room_id = RoomId::from_proto(
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
.await
.unwrap()
.id,
);
db.call(
room_id,
user1.user_id,
ConnectionId { owner_id, id: 0 },
user2.user_id,
None,
)
.await
.unwrap();
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(
room_id,
ConnectionId { owner_id, id: 1 },
&[],
false,
false,
&[],
)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
db.share_project(
room_id,
ConnectionId { owner_id, id: 1 },
&[],
false,
false,
&[],
)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
// Projects shared by admins aren't counted.
db.share_project(
room_id,
ConnectionId { owner_id, id: 0 },
&[],
false,
false,
&[],
)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
db.leave_room(ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
}
test_both_dbs!(
test_upsert_shared_thread,
test_upsert_shared_thread_postgres,
test_upsert_shared_thread_sqlite
);
async fn test_upsert_shared_thread(db: &Arc<Database>) {
use collab::db::SharedThreadId;
use uuid::Uuid;
let user_id = new_test_user(db, "user1@example.com").await;
let thread_id = SharedThreadId(Uuid::new_v4());
let title = "My Test Thread";
let data = b"test thread data".to_vec();
db.upsert_shared_thread(thread_id, user_id, title, data.clone())
.await
.unwrap();
let result = db.get_shared_thread(thread_id).await.unwrap();
assert!(result.is_some(), "Should find the shared thread");
let (thread, username) = result.unwrap();
assert_eq!(thread.title, title);
assert_eq!(thread.data, data);
assert_eq!(thread.user_id, user_id);
assert_eq!(username, "user1");
}
test_both_dbs!(
test_upsert_shared_thread_updates_existing,
test_upsert_shared_thread_updates_existing_postgres,
test_upsert_shared_thread_updates_existing_sqlite
);
async fn test_upsert_shared_thread_updates_existing(db: &Arc<Database>) {
use collab::db::SharedThreadId;
use uuid::Uuid;
let user_id = new_test_user(db, "user1@example.com").await;
let thread_id = SharedThreadId(Uuid::new_v4());
// Create initial thread.
db.upsert_shared_thread(
thread_id,
user_id,
"Original Title",
b"original data".to_vec(),
)
.await
.unwrap();
// Update the same thread.
db.upsert_shared_thread(
thread_id,
user_id,
"Updated Title",
b"updated data".to_vec(),
)
.await
.unwrap();
let result = db.get_shared_thread(thread_id).await.unwrap();
let (thread, _) = result.unwrap();
assert_eq!(thread.title, "Updated Title");
assert_eq!(thread.data, b"updated data".to_vec());
}
test_both_dbs!(
test_cannot_update_another_users_shared_thread,
test_cannot_update_another_users_shared_thread_postgres,
test_cannot_update_another_users_shared_thread_sqlite
);
async fn test_cannot_update_another_users_shared_thread(db: &Arc<Database>) {
use collab::db::SharedThreadId;
use uuid::Uuid;
let user1_id = new_test_user(db, "user1@example.com").await;
let user2_id = new_test_user(db, "user2@example.com").await;
let thread_id = SharedThreadId(Uuid::new_v4());
db.upsert_shared_thread(thread_id, user1_id, "User 1 Thread", b"user1 data".to_vec())
.await
.unwrap();
let result = db
.upsert_shared_thread(thread_id, user2_id, "User 2 Title", b"user2 data".to_vec())
.await;
assert!(
result.is_err(),
"Should not allow updating another user's thread"
);
}
test_both_dbs!(
test_get_nonexistent_shared_thread,
test_get_nonexistent_shared_thread_postgres,
test_get_nonexistent_shared_thread_sqlite
);
async fn test_get_nonexistent_shared_thread(db: &Arc<Database>) {
use collab::db::SharedThreadId;
use uuid::Uuid;
let result = db
.get_shared_thread(SharedThreadId(Uuid::new_v4()))
.await
.unwrap();
assert!(result.is_none(), "Should not find non-existent thread");
}

View File

@@ -0,0 +1,131 @@
use std::collections::BTreeSet;
use std::sync::Arc;
use cloud_api_types::{ExtensionMetadata, ExtensionProvides};
use collab::db::Database;
use collab::db::ExtensionVersionConstraints;
use collab::db::{NewExtensionVersion, queries::extensions::convert_time_to_chrono};
use crate::test_both_dbs;
test_both_dbs!(
test_extensions_by_id,
test_extensions_by_id_postgres,
test_extensions_by_id_sqlite
);
async fn test_extensions_by_id(db: &Arc<Database>) {
let versions = db.get_known_extension_versions().await.unwrap();
assert!(versions.is_empty());
let t0 = time::OffsetDateTime::from_unix_timestamp_nanos(0).unwrap();
let t0 = time::PrimitiveDateTime::new(t0.date(), t0.time());
let t0_chrono = convert_time_to_chrono(t0);
db.insert_extension_versions(
&[
(
"ext1",
vec![
NewExtensionVersion {
name: "Extension 1".into(),
version: semver::Version::parse("0.0.1").unwrap(),
description: "an extension".into(),
authors: vec!["max".into()],
repository: "ext1/repo".into(),
schema_version: 1,
wasm_api_version: Some("0.0.4".into()),
provides: BTreeSet::from_iter([
ExtensionProvides::Grammars,
ExtensionProvides::Languages,
]),
published_at: t0,
},
NewExtensionVersion {
name: "Extension 1".into(),
version: semver::Version::parse("0.0.2").unwrap(),
description: "a good extension".into(),
authors: vec!["max".into()],
repository: "ext1/repo".into(),
schema_version: 1,
wasm_api_version: Some("0.0.4".into()),
provides: BTreeSet::from_iter([
ExtensionProvides::Grammars,
ExtensionProvides::Languages,
ExtensionProvides::LanguageServers,
]),
published_at: t0,
},
NewExtensionVersion {
name: "Extension 1".into(),
version: semver::Version::parse("0.0.3").unwrap(),
description: "a real good extension".into(),
authors: vec!["max".into(), "marshall".into()],
repository: "ext1/repo".into(),
schema_version: 1,
wasm_api_version: Some("0.0.5".into()),
provides: BTreeSet::from_iter([
ExtensionProvides::Grammars,
ExtensionProvides::Languages,
ExtensionProvides::LanguageServers,
]),
published_at: t0,
},
],
),
(
"ext2",
vec![NewExtensionVersion {
name: "Extension 2".into(),
version: semver::Version::parse("0.2.0").unwrap(),
description: "a great extension".into(),
authors: vec!["marshall".into()],
repository: "ext2/repo".into(),
schema_version: 0,
wasm_api_version: None,
provides: BTreeSet::default(),
published_at: t0,
}],
),
]
.into_iter()
.collect(),
)
.await
.unwrap();
let extensions = db
.get_extensions_by_ids(
&["ext1"],
Some(&ExtensionVersionConstraints {
schema_versions: 1..=1,
wasm_api_versions: "0.0.1".parse().unwrap()..="0.0.4".parse().unwrap(),
}),
)
.await
.unwrap();
assert_eq!(
extensions,
&[ExtensionMetadata {
id: "ext1".into(),
manifest: cloud_api_types::ExtensionApiManifest {
name: "Extension 1".into(),
version: "0.0.2".into(),
authors: vec!["max".into()],
description: Some("a good extension".into()),
repository: "ext1/repo".into(),
schema_version: Some(1),
wasm_api_version: Some("0.0.4".into()),
provides: BTreeSet::from_iter([
ExtensionProvides::Grammars,
ExtensionProvides::Languages,
ExtensionProvides::LanguageServers,
]),
},
published_at: t0_chrono,
download_count: 0,
}]
);
}

View File

@@ -0,0 +1,47 @@
use std::path::Path;
use std::time::Duration;
use anyhow::{Result, anyhow};
use collections::HashMap;
use sea_orm::ConnectOptions;
use sqlx::Connection;
use sqlx::migrate::{Migrate, Migration, MigrationSource};
/// Runs the database migrations for the specified database.
pub async fn run_database_migrations(
database_options: &ConnectOptions,
migrations_path: impl AsRef<Path>,
) -> Result<Vec<(Migration, Duration)>> {
let migrations = MigrationSource::resolve(migrations_path.as_ref())
.await
.map_err(|err| anyhow!("failed to load migrations: {err:?}"))?;
let mut connection = sqlx::AnyConnection::connect(database_options.get_url()).await?;
connection.ensure_migrations_table().await?;
let applied_migrations: HashMap<_, _> = connection
.list_applied_migrations()
.await?
.into_iter()
.map(|migration| (migration.version, migration))
.collect();
let mut new_migrations = Vec::new();
for migration in migrations {
match applied_migrations.get(&migration.version) {
Some(applied_migration) => {
anyhow::ensure!(
migration.checksum == applied_migration.checksum,
"checksum mismatch for applied migration {}",
migration.description
);
}
None => {
let elapsed = connection.apply(&migration).await?;
new_migrations.push((migration, elapsed));
}
}
}
Ok(new_migrations)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,164 @@
use std::sync::Arc;
use gpui::{BackgroundExecutor, TestAppContext};
use notifications::NotificationEvent;
use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use rpc::{Notification, proto};
use crate::TestServer;
#[gpui::test]
async fn test_notifications(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
// Wait for authentication/connection to Collab to be established.
executor.run_until_parked();
let notification_events_a = Arc::new(Mutex::new(Vec::new()));
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
client_a.notification_store().update(cx_a, |_, cx| {
let events = notification_events_a.clone();
cx.subscribe(&cx.entity(), move |_, _, event, _| {
events.lock().push(event.clone());
})
.detach()
});
client_b.notification_store().update(cx_b, |_, cx| {
let events = notification_events_b.clone();
cx.subscribe(&cx.entity(), move |_, _, event, _| {
events.lock().push(event.clone());
})
.detach()
});
// Client A sends a contact request to client B.
client_a
.user_store()
.update(cx_a, |store, cx| store.request_contact(client_b.id(), cx))
.await
.unwrap();
// Client B receives a contact request notification and responds to the
// request, accepting it.
executor.run_until_parked();
client_b.notification_store().update(cx_b, |store, cx| {
assert_eq!(store.notification_count(), 1);
assert_eq!(store.unread_notification_count(), 1);
let entry = store.notification_at(0).unwrap();
assert_eq!(
entry.notification,
Notification::ContactRequest {
sender_id: client_a.id()
}
);
assert!(!entry.is_read);
assert_eq!(
&notification_events_b.lock()[0..],
&[
NotificationEvent::NewNotification {
entry: entry.clone(),
},
NotificationEvent::NotificationsUpdated {
old_range: 0..0,
new_count: 1
}
]
);
store.respond_to_notification(entry.notification.clone(), true, cx);
});
// Client B sees the notification is now read, and that they responded.
executor.run_until_parked();
client_b.notification_store().read_with(cx_b, |store, _| {
assert_eq!(store.notification_count(), 1);
assert_eq!(store.unread_notification_count(), 0);
let entry = store.notification_at(0).unwrap();
assert!(entry.is_read);
assert_eq!(entry.response, Some(true));
assert_eq!(
&notification_events_b.lock()[2..],
&[
NotificationEvent::NotificationRead {
entry: entry.clone(),
},
NotificationEvent::NotificationsUpdated {
old_range: 0..1,
new_count: 1
}
]
);
});
// Client A receives a notification that client B accepted their request.
client_a.notification_store().read_with(cx_a, |store, _| {
assert_eq!(store.notification_count(), 1);
assert_eq!(store.unread_notification_count(), 1);
let entry = store.notification_at(0).unwrap();
assert_eq!(
entry.notification,
Notification::ContactRequestAccepted {
responder_id: client_b.id()
}
);
assert!(!entry.is_read);
});
// Client A creates a channel and invites client B to be a member.
let channel_id = client_a
.channel_store()
.update(cx_a, |store, cx| {
store.create_channel("the-channel", None, cx)
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |store, cx| {
store.invite_member(channel_id, client_b.id(), proto::ChannelRole::Member, cx)
})
.await
.unwrap();
// Client B receives a channel invitation notification and responds to the
// invitation, accepting it.
executor.run_until_parked();
client_b.notification_store().update(cx_b, |store, cx| {
assert_eq!(store.notification_count(), 2);
assert_eq!(store.unread_notification_count(), 1);
let entry = store.notification_at(0).unwrap();
assert_eq!(
entry.notification,
Notification::ChannelInvitation {
channel_id: channel_id.0,
channel_name: "the-channel".to_string(),
inviter_id: client_a.id()
}
);
assert!(!entry.is_read);
store.respond_to_notification(entry.notification.clone(), true, cx);
});
// Client B sees the notification is now read, and that they responded.
executor.run_until_parked();
client_b.notification_store().read_with(cx_b, |store, _| {
assert_eq!(store.notification_count(), 2);
assert_eq!(store.unread_notification_count(), 0);
let entry = store.notification_at(0).unwrap();
assert!(entry.is_read);
assert_eq!(entry.response, Some(true));
});
}

View File

@@ -0,0 +1,285 @@
use collab::db::*;
use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan, run_randomized_test};
use anyhow::Result;
use async_trait::async_trait;
use gpui::{BackgroundExecutor, SharedString, TestAppContext};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
use std::{
ops::{Deref, DerefMut, Range},
rc::Rc,
sync::Arc,
};
use text::Bias;
#[gpui::test(iterations = 100, on_failure = "crate::save_randomized_test_plan")]
async fn test_random_channel_buffers(
cx: &mut TestAppContext,
executor: BackgroundExecutor,
rng: StdRng,
) {
run_randomized_test::<RandomChannelBufferTest>(cx, executor, rng).await;
}
struct RandomChannelBufferTest;
#[derive(Clone, Serialize, Deserialize)]
enum ChannelBufferOperation {
JoinChannelNotes {
channel_name: SharedString,
},
LeaveChannelNotes {
channel_name: SharedString,
},
EditChannelNotes {
channel_name: SharedString,
edits: Vec<(Range<usize>, Arc<str>)>,
},
Noop,
}
const CHANNEL_COUNT: usize = 3;
#[async_trait(?Send)]
impl RandomizedTest for RandomChannelBufferTest {
type Operation = ChannelBufferOperation;
async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) {
let db = &server.app_state.db;
for ix in 0..CHANNEL_COUNT {
let id = db
.create_root_channel(&format!("channel-{ix}"), users[0].user_id)
.await
.unwrap();
for user in &users[1..] {
db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member)
.await
.unwrap();
db.respond_to_channel_invite(id, user.user_id, true)
.await
.unwrap();
}
}
}
fn generate_operation(
client: &TestClient,
rng: &mut StdRng,
_: &mut UserTestPlan,
cx: &TestAppContext,
) -> ChannelBufferOperation {
let channel_store = client.channel_store().clone();
let mut channel_buffers = client.channel_buffers();
// When signed out, we can't do anything unless a channel buffer is
// already open.
if channel_buffers.deref_mut().is_empty()
&& channel_store.read_with(cx, |store, _| store.channel_count() == 0)
{
return ChannelBufferOperation::Noop;
}
loop {
match rng.random_range(0..100_u32) {
0..=29 => {
let channel_name = client.channel_store().read_with(cx, |store, cx| {
store.ordered_channels().find_map(|(_, channel)| {
if store.has_open_channel_buffer(channel.id, cx) {
None
} else {
Some(channel.name.clone())
}
})
});
if let Some(channel_name) = channel_name {
break ChannelBufferOperation::JoinChannelNotes { channel_name };
}
}
30..=40 => {
if let Some(buffer) = channel_buffers.deref().iter().choose(rng) {
let channel_name =
buffer.read_with(cx, |b, cx| b.channel(cx).unwrap().name.clone());
break ChannelBufferOperation::LeaveChannelNotes { channel_name };
}
}
_ => {
if let Some(buffer) = channel_buffers.deref().iter().choose(rng) {
break buffer.read_with(cx, |b, cx| {
let channel_name = b.channel(cx).unwrap().name.clone();
let edits = b
.buffer()
.read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
ChannelBufferOperation::EditChannelNotes {
channel_name,
edits,
}
});
}
}
}
}
}
async fn apply_operation(
client: &TestClient,
operation: ChannelBufferOperation,
cx: &mut TestAppContext,
) -> Result<(), TestError> {
match operation {
ChannelBufferOperation::JoinChannelNotes { channel_name } => {
let buffer = client.channel_store().update(cx, |store, cx| {
let channel_id = store
.ordered_channels()
.find(|(_, c)| c.name == channel_name)
.unwrap()
.1
.id;
if store.has_open_channel_buffer(channel_id, cx) {
Err(TestError::Inapplicable)
} else {
Ok(store.open_channel_buffer(channel_id, cx))
}
})?;
log::info!(
"{}: opening notes for channel {channel_name}",
client.username
);
client.channel_buffers().deref_mut().insert(buffer.await?);
}
ChannelBufferOperation::LeaveChannelNotes { channel_name } => {
let buffer = cx.update(|cx| {
let mut left_buffer = Err(TestError::Inapplicable);
client.channel_buffers().deref_mut().retain(|buffer| {
if buffer.read(cx).channel(cx).unwrap().name == channel_name {
left_buffer = Ok(buffer.clone());
false
} else {
true
}
});
left_buffer
})?;
log::info!(
"{}: closing notes for channel {channel_name}",
client.username
);
cx.update(|_| drop(buffer));
}
ChannelBufferOperation::EditChannelNotes {
channel_name,
edits,
} => {
let channel_buffer = cx
.read(|cx| {
client
.channel_buffers()
.deref()
.iter()
.find(|buffer| {
buffer.read(cx).channel(cx).unwrap().name == channel_name
})
.cloned()
})
.ok_or_else(|| TestError::Inapplicable)?;
log::info!(
"{}: editing notes for channel {channel_name} with {:?}",
client.username,
edits
);
channel_buffer.update(cx, |buffer, cx| {
let buffer = buffer.buffer();
buffer.update(cx, |buffer, cx| {
let snapshot = buffer.snapshot();
buffer.edit(
edits.into_iter().map(|(range, text)| {
let start = snapshot.clip_offset(range.start, Bias::Left);
let end = snapshot.clip_offset(range.end, Bias::Right);
(start..end, text)
}),
None,
cx,
);
});
});
}
ChannelBufferOperation::Noop => Err(TestError::Inapplicable)?,
}
Ok(())
}
async fn on_quiesce(server: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
let channels = server.app_state.db.all_channels().await.unwrap();
for (client, client_cx) in clients.iter_mut() {
client_cx.update(|cx| {
client
.channel_buffers()
.deref_mut()
.retain(|b| b.read(cx).is_connected());
});
}
for (channel_id, channel_name) in channels {
let mut prev_text: Option<(u64, String)> = None;
let mut collaborator_user_ids = server
.app_state
.db
.get_channel_buffer_collaborators(channel_id)
.await
.unwrap()
.into_iter()
.map(|id| id.to_proto())
.collect::<Vec<_>>();
collaborator_user_ids.sort();
for (client, client_cx) in clients.iter() {
let user_id = client.user_id().unwrap();
client_cx.read(|cx| {
if let Some(channel_buffer) = client
.channel_buffers()
.deref()
.iter()
.find(|b| b.read(cx).channel_id.0 == channel_id.to_proto())
{
let channel_buffer = channel_buffer.read(cx);
// Assert that channel buffer's text matches other clients' copies.
let text = channel_buffer.buffer().read(cx).text();
if let Some((prev_user_id, prev_text)) = &prev_text {
assert_eq!(
&text,
prev_text,
"client {user_id} has different text than client {prev_user_id} for channel {channel_name}",
);
} else {
prev_text = Some((user_id, text));
}
// Assert that all clients and the server agree about who is present in the
// channel buffer.
let collaborators = channel_buffer.collaborators();
let mut user_ids =
collaborators.values().map(|c| c.user_id).collect::<Vec<_>>();
user_ids.sort();
assert_eq!(
user_ids,
collaborator_user_ids,
"client {user_id} has different user ids for channel {channel_name} than the server",
);
}
});
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,702 @@
use crate::{TestClient, TestServer};
use async_trait::async_trait;
use collab::{
db::{self, NewUserParams, UserId},
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
};
use futures::StreamExt;
use gpui::{BackgroundExecutor, Task, TestAppContext};
use parking_lot::Mutex;
use rand::prelude::*;
use rpc::RECEIVE_TIMEOUT;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use settings::SettingsStore;
use std::sync::OnceLock;
use std::{
env,
path::PathBuf,
rc::Rc,
sync::{
Arc,
atomic::{AtomicBool, Ordering::SeqCst},
},
};
fn plan_load_path() -> &'static Option<PathBuf> {
static PLAN_LOAD_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
PLAN_LOAD_PATH.get_or_init(|| path_env_var("LOAD_PLAN"))
}
fn plan_save_path() -> &'static Option<PathBuf> {
static PLAN_SAVE_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
PLAN_SAVE_PATH.get_or_init(|| path_env_var("SAVE_PLAN"))
}
fn max_peers() -> usize {
static MAX_PEERS: OnceLock<usize> = OnceLock::new();
*MAX_PEERS.get_or_init(|| {
env::var("MAX_PEERS")
.map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
.unwrap_or(3)
})
}
fn max_operations() -> usize {
static MAX_OPERATIONS: OnceLock<usize> = OnceLock::new();
*MAX_OPERATIONS.get_or_init(|| {
env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10)
})
}
static LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
static LAST_PLAN: Mutex<Option<Box<dyn Send + FnOnce() -> Vec<u8>>>> = Mutex::new(None);
struct TestPlan<T: RandomizedTest> {
rng: StdRng,
replay: bool,
stored_operations: Vec<(StoredOperation<T::Operation>, Arc<AtomicBool>)>,
max_operations: usize,
operation_ix: usize,
users: Vec<UserTestPlan>,
next_batch_id: usize,
allow_server_restarts: bool,
allow_client_reconnection: bool,
allow_client_disconnection: bool,
}
pub struct UserTestPlan {
pub user_id: UserId,
pub username: String,
pub allow_client_disconnection: bool,
next_root_id: usize,
operation_ix: usize,
online: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum StoredOperation<T> {
Server(ServerOperation),
Client {
user_id: UserId,
batch_id: usize,
operation: T,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum ServerOperation {
AddConnection {
user_id: UserId,
},
RemoveConnection {
user_id: UserId,
},
BounceConnection {
user_id: UserId,
},
RestartServer,
MutateClients {
batch_id: usize,
#[serde(skip_serializing)]
#[serde(skip_deserializing)]
user_ids: Vec<UserId>,
quiesce: bool,
},
}
pub enum TestError {
Inapplicable,
Other(anyhow::Error),
}
#[async_trait(?Send)]
pub trait RandomizedTest: 'static + Sized {
type Operation: Send + Clone + Serialize + DeserializeOwned;
fn generate_operation(
client: &TestClient,
rng: &mut StdRng,
plan: &mut UserTestPlan,
cx: &TestAppContext,
) -> Self::Operation;
async fn apply_operation(
client: &TestClient,
operation: Self::Operation,
cx: &mut TestAppContext,
) -> Result<(), TestError>;
async fn initialize(server: &mut TestServer, users: &[UserTestPlan]);
async fn on_client_added(_client: &Rc<TestClient>, _cx: &mut TestAppContext) {}
async fn on_quiesce(server: &mut TestServer, client: &mut [(Rc<TestClient>, TestAppContext)]);
}
pub async fn run_randomized_test<T: RandomizedTest>(
cx: &mut TestAppContext,
executor: BackgroundExecutor,
rng: StdRng,
) {
let mut server = TestServer::start(executor.clone()).await;
let plan = TestPlan::<T>::new(&mut server, rng).await;
LAST_PLAN.lock().replace({
let plan = plan.clone();
Box::new(move || plan.lock().serialize())
});
let mut clients = Vec::new();
let mut client_tasks = Vec::new();
let mut operation_channels = Vec::new();
loop {
let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else {
break;
};
applied.store(true, SeqCst);
let did_apply = TestPlan::apply_server_operation(
plan.clone(),
executor.clone(),
&mut server,
&mut clients,
&mut client_tasks,
&mut operation_channels,
next_operation,
cx,
)
.await;
if !did_apply {
applied.store(false, SeqCst);
}
}
drop(operation_channels);
futures::future::join_all(client_tasks).await;
executor.run_until_parked();
T::on_quiesce(&mut server, &mut clients).await;
for (client, cx) in clients {
cx.update(|cx| {
for window in cx.windows() {
window
.update(cx, |_, window, _| window.remove_window())
.ok();
}
});
cx.update(|cx| {
let settings = cx.remove_global::<SettingsStore>();
cx.clear_globals();
cx.set_global(settings);
theme_settings::init(theme::LoadThemes::JustBase, cx);
drop(client);
});
executor.run_until_parked();
}
if let Some(path) = plan_save_path() {
eprintln!("saved test plan to path {:?}", path);
std::fs::write(path, plan.lock().serialize()).unwrap();
}
}
pub fn save_randomized_test_plan() {
if let Some(serialize_plan) = LAST_PLAN.lock().take()
&& let Some(path) = plan_save_path()
{
eprintln!("saved test plan to path {:?}", path);
std::fs::write(path, serialize_plan()).unwrap();
}
}
impl<T: RandomizedTest> TestPlan<T> {
pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc<Mutex<Self>> {
let allow_server_restarts = rng.random_bool(0.7);
let allow_client_reconnection = rng.random_bool(0.7);
let allow_client_disconnection = rng.random_bool(0.1);
let mut users = Vec::new();
for ix in 0..max_peers() {
let username = format!("user-{}", ix + 1);
let user_id = server
.app_state
.db
.create_user(
&format!("{username}@example.com"),
None,
false,
NewUserParams {
github_login: username.clone(),
github_user_id: ix as i32,
},
)
.await
.unwrap()
.user_id;
users.push(UserTestPlan {
user_id,
username,
online: false,
next_root_id: 0,
operation_ix: 0,
allow_client_disconnection,
});
}
T::initialize(server, &users).await;
let plan = Arc::new(Mutex::new(Self {
replay: false,
allow_server_restarts,
allow_client_reconnection,
allow_client_disconnection,
stored_operations: Vec::new(),
operation_ix: 0,
next_batch_id: 0,
max_operations: max_operations(),
users,
rng,
}));
if let Some(path) = plan_load_path() {
let json = LOADED_PLAN_JSON
.lock()
.get_or_insert_with(|| {
eprintln!("loaded test plan from path {:?}", path);
std::fs::read(path).unwrap()
})
.clone();
plan.lock().deserialize(json);
}
plan
}
fn deserialize(&mut self, json: Vec<u8>) {
let stored_operations: Vec<StoredOperation<T::Operation>> =
serde_json::from_slice(&json).unwrap();
self.replay = true;
self.stored_operations = stored_operations
.iter()
.cloned()
.enumerate()
.map(|(i, mut operation)| {
let did_apply = Arc::new(AtomicBool::new(false));
if let StoredOperation::Server(ServerOperation::MutateClients {
batch_id: current_batch_id,
user_ids,
..
}) = &mut operation
{
assert!(user_ids.is_empty());
user_ids.extend(stored_operations[i + 1..].iter().filter_map(|operation| {
if let StoredOperation::Client {
user_id, batch_id, ..
} = operation
&& batch_id == current_batch_id
{
return Some(user_id);
}
None
}));
user_ids.sort_unstable();
}
(operation, did_apply)
})
.collect()
}
fn serialize(&mut self) -> Vec<u8> {
// Format each operation as one line
let mut json = Vec::new();
json.push(b'[');
for (operation, applied) in &self.stored_operations {
if !applied.load(SeqCst) {
continue;
}
if json.len() > 1 {
json.push(b',');
}
json.extend_from_slice(b"\n ");
serde_json::to_writer(&mut json, operation).unwrap();
}
json.extend_from_slice(b"\n]\n");
json
}
fn next_server_operation(
&mut self,
clients: &[(Rc<TestClient>, TestAppContext)],
) -> Option<(ServerOperation, Arc<AtomicBool>)> {
if self.replay {
while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) {
self.operation_ix += 1;
if let (StoredOperation::Server(operation), applied) = stored_operation {
return Some((operation.clone(), applied.clone()));
}
}
None
} else {
let operation = self.generate_server_operation(clients)?;
let applied = Arc::new(AtomicBool::new(false));
self.stored_operations
.push((StoredOperation::Server(operation.clone()), applied.clone()));
Some((operation, applied))
}
}
fn next_client_operation(
&mut self,
client: &TestClient,
current_batch_id: usize,
cx: &TestAppContext,
) -> Option<(T::Operation, Arc<AtomicBool>)> {
let current_user_id = client.current_user_id(cx);
let user_ix = self
.users
.iter()
.position(|user| user.user_id == current_user_id)
.unwrap();
let user_plan = &mut self.users[user_ix];
if self.replay {
while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) {
user_plan.operation_ix += 1;
if let (
StoredOperation::Client {
user_id, operation, ..
},
applied,
) = stored_operation
&& user_id == &current_user_id
{
return Some((operation.clone(), applied.clone()));
}
}
None
} else {
if self.operation_ix == self.max_operations {
return None;
}
self.operation_ix += 1;
let operation = T::generate_operation(
client,
&mut self.rng,
self.users
.iter_mut()
.find(|user| user.user_id == current_user_id)
.unwrap(),
cx,
);
let applied = Arc::new(AtomicBool::new(false));
self.stored_operations.push((
StoredOperation::Client {
user_id: current_user_id,
batch_id: current_batch_id,
operation: operation.clone(),
},
applied.clone(),
));
Some((operation, applied))
}
}
fn generate_server_operation(
&mut self,
clients: &[(Rc<TestClient>, TestAppContext)],
) -> Option<ServerOperation> {
if self.operation_ix == self.max_operations {
return None;
}
Some(loop {
break match self.rng.random_range(0..100) {
0..=29 if clients.len() < self.users.len() => {
let user = self
.users
.iter()
.filter(|u| !u.online)
.choose(&mut self.rng)
.unwrap();
self.operation_ix += 1;
ServerOperation::AddConnection {
user_id: user.user_id,
}
}
30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
let user_id = client.current_user_id(cx);
self.operation_ix += 1;
ServerOperation::RemoveConnection { user_id }
}
35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
let user_id = client.current_user_id(cx);
self.operation_ix += 1;
ServerOperation::BounceConnection { user_id }
}
40..=44 if self.allow_server_restarts && clients.len() > 1 => {
self.operation_ix += 1;
ServerOperation::RestartServer
}
_ if !clients.is_empty() => {
let count = self
.rng
.random_range(1..10)
.min(self.max_operations - self.operation_ix);
let batch_id = util::post_inc(&mut self.next_batch_id);
let mut user_ids = (0..count)
.map(|_| {
let ix = self.rng.random_range(0..clients.len());
let (client, cx) = &clients[ix];
client.current_user_id(cx)
})
.collect::<Vec<_>>();
user_ids.sort_unstable();
ServerOperation::MutateClients {
user_ids,
batch_id,
quiesce: self.rng.random_bool(0.7),
}
}
_ => continue,
};
})
}
async fn apply_server_operation(
plan: Arc<Mutex<Self>>,
deterministic: BackgroundExecutor,
server: &mut TestServer,
clients: &mut Vec<(Rc<TestClient>, TestAppContext)>,
client_tasks: &mut Vec<Task<()>>,
operation_channels: &mut Vec<futures::channel::mpsc::UnboundedSender<usize>>,
operation: ServerOperation,
cx: &mut TestAppContext,
) -> bool {
match operation {
ServerOperation::AddConnection { user_id } => {
let username;
{
let mut plan = plan.lock();
let user = plan.user(user_id);
if user.online {
return false;
}
user.online = true;
username = user.username.clone();
};
log::info!("adding new connection for {}", username);
let mut client_cx = cx.new_app();
let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded();
let client = Rc::new(server.create_client(&mut client_cx, &username).await);
operation_channels.push(operation_tx);
clients.push((client.clone(), client_cx.clone()));
let foreground_executor = client_cx.foreground_executor().clone();
let simulate_client =
Self::simulate_client(plan.clone(), client, operation_rx, client_cx);
client_tasks.push(foreground_executor.spawn(simulate_client));
log::info!("added connection for {}", username);
}
ServerOperation::RemoveConnection {
user_id: removed_user_id,
} => {
log::info!("simulating full disconnection of user {}", removed_user_id);
let client_ix = clients
.iter()
.position(|(client, cx)| client.current_user_id(cx) == removed_user_id);
let Some(client_ix) = client_ix else {
return false;
};
let user_connection_ids = server
.connection_pool
.lock()
.user_connection_ids(removed_user_id)
.collect::<Vec<_>>();
assert_eq!(user_connection_ids.len(), 1);
let removed_peer_id = user_connection_ids[0].into();
let (client, client_cx) = clients.remove(client_ix);
let client_task = client_tasks.remove(client_ix);
operation_channels.remove(client_ix);
server.forbid_connections();
server.disconnect_client(removed_peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
log::info!("waiting for user {} to exit...", removed_user_id);
client_task.await;
server.allow_connections();
for project in client.dev_server_projects().iter() {
project.read_with(&client_cx, |project, cx| {
assert!(
project.is_disconnected(cx),
"project {:?} should be read only",
project.remote_id()
)
});
}
for (client, cx) in clients {
let contacts = server
.app_state
.db
.get_contacts(client.current_user_id(cx))
.await
.unwrap();
let pool = server.connection_pool.lock();
for contact in contacts {
if let db::Contact::Accepted { user_id, busy, .. } = contact
&& user_id == removed_user_id
{
assert!(!pool.is_user_online(user_id));
assert!(!busy);
}
}
}
log::info!("{} removed", client.username);
plan.lock().user(removed_user_id).online = false;
client_cx.update(|cx| {
for window in cx.windows() {
window
.update(cx, |_, window, _| window.remove_window())
.ok();
}
});
client_cx.update(|cx| {
cx.clear_globals();
drop(client);
});
}
ServerOperation::BounceConnection { user_id } => {
log::info!("simulating temporary disconnection of user {}", user_id);
let user_connection_ids = server
.connection_pool
.lock()
.user_connection_ids(user_id)
.collect::<Vec<_>>();
if user_connection_ids.is_empty() {
return false;
}
assert_eq!(user_connection_ids.len(), 1);
let peer_id = user_connection_ids[0].into();
server.disconnect_client(peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
}
ServerOperation::RestartServer => {
log::info!("simulating server restart");
server.reset().await;
deterministic.advance_clock(RECEIVE_TIMEOUT);
server.start().await.unwrap();
deterministic.advance_clock(CLEANUP_TIMEOUT);
let environment = &server.app_state.config.zed_environment;
let (stale_room_ids, _) = server
.app_state
.db
.stale_server_resource_ids(environment, server.id())
.await
.unwrap();
assert_eq!(stale_room_ids, vec![]);
}
ServerOperation::MutateClients {
user_ids,
batch_id,
quiesce,
} => {
let mut applied = false;
for user_id in user_ids {
let client_ix = clients
.iter()
.position(|(client, cx)| client.current_user_id(cx) == user_id);
let Some(client_ix) = client_ix else { continue };
applied = true;
if let Err(err) = operation_channels[client_ix].unbounded_send(batch_id) {
log::error!("error signaling user {user_id}: {err}");
}
}
if quiesce && applied {
deterministic.run_until_parked();
T::on_quiesce(server, clients).await;
}
return applied;
}
}
true
}
async fn simulate_client(
plan: Arc<Mutex<Self>>,
client: Rc<TestClient>,
mut operation_rx: futures::channel::mpsc::UnboundedReceiver<usize>,
mut cx: TestAppContext,
) {
T::on_client_added(&client, &mut cx).await;
while let Some(batch_id) = operation_rx.next().await {
let Some((operation, applied)) =
plan.lock().next_client_operation(&client, batch_id, &cx)
else {
break;
};
applied.store(true, SeqCst);
match T::apply_operation(&client, operation, &mut cx).await {
Ok(()) => {}
Err(TestError::Inapplicable) => {
applied.store(false, SeqCst);
log::info!("skipped operation");
}
Err(TestError::Other(error)) => {
log::error!("{} error: {}", client.username, error);
}
}
cx.executor().simulate_random_delay().await;
}
log::info!("{}: done", client.username);
}
fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
self.users
.iter_mut()
.find(|user| user.user_id == user_id)
.unwrap()
}
}
impl UserTestPlan {
pub fn next_root_dir_name(&mut self) -> String {
let user_id = self.user_id;
let root_id = util::post_inc(&mut self.next_root_id);
format!("dir-{user_id}-{root_id}")
}
}
impl From<anyhow::Error> for TestError {
fn from(value: anyhow::Error) -> Self {
Self::Other(value)
}
}
fn path_env_var(name: &str) -> Option<PathBuf> {
let value = env::var(name).ok()?;
let mut path = PathBuf::from(value);
if path.is_relative() {
let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
abs_path.pop();
abs_path.pop();
abs_path.push(path);
path = abs_path
}
Some(path)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,976 @@
use anyhow::anyhow;
use call::ActiveCall;
use channel::{ChannelBuffer, ChannelStore};
use client::test::{make_get_authenticated_user_response, parse_authorization_header};
use client::{
self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
proto::PeerId,
};
use clock::FakeSystemClock;
use collab::services::{FakeUserService, NewUserParams};
use collab::{
AppState, Config,
db::UserId,
executor::Executor,
rpc::{CLEANUP_TIMEOUT, Principal, RECONNECT_TIMEOUT, Server, ZedVersion},
};
use collab_ui::channel_view::ChannelView;
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{StreamExt as _, channel::oneshot};
use git::GitHostingProviderRegistry;
use gpui::{AppContext as _, BackgroundExecutor, Entity, Task, TestAppContext, VisualTestContext};
use http_client::{FakeHttpClient, Method};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use notifications::NotificationStore;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
use remote::RemoteClient;
use rpc::{
RECEIVE_TIMEOUT,
proto::{self, ChannelRole},
};
use serde_json::json;
use session::{AppSession, Session};
use settings::SettingsStore;
use std::{
cell::{Ref, RefCell, RefMut},
env,
ops::{Deref, DerefMut},
path::Path,
sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
},
};
use util::path;
use workspace::{MultiWorkspace, Workspace, WorkspaceStore};
use livekit_client::test::TestServer as LivekitTestServer;
use crate::db_tests::TestDb;
pub struct TestServer {
pub app_state: Arc<AppState>,
pub test_livekit_server: Arc<LivekitTestServer>,
pub test_db: TestDb,
server: Arc<Server>,
next_github_user_id: i32,
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
forbid_connections: Arc<AtomicBool>,
}
pub struct TestClient {
pub username: String,
pub app_state: Arc<workspace::AppState>,
channel_store: Entity<ChannelStore>,
notification_store: Entity<NotificationStore>,
state: RefCell<TestClientState>,
}
#[derive(Default)]
struct TestClientState {
local_projects: Vec<Entity<Project>>,
dev_server_projects: Vec<Entity<Project>>,
buffers: HashMap<Entity<Project>, HashSet<Entity<language::Buffer>>>,
channel_buffers: HashSet<Entity<ChannelBuffer>>,
}
pub struct ContactsSummary {
pub current: Vec<String>,
pub outgoing_requests: Vec<String>,
pub incoming_requests: Vec<String>,
}
impl TestServer {
pub async fn start(deterministic: BackgroundExecutor) -> Self {
static NEXT_LIVEKIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
let use_postgres = env::var("USE_POSTGRES").ok();
let use_postgres = use_postgres.as_deref();
let test_db = if use_postgres == Some("true") || use_postgres == Some("1") {
TestDb::postgres(deterministic.clone())
} else {
TestDb::sqlite(deterministic.clone())
};
let livekit_server_id = NEXT_LIVEKIT_SERVER_ID.fetch_add(1, SeqCst);
let livekit_server = LivekitTestServer::create(
format!("http://livekit.{}.test", livekit_server_id),
format!("devkey-{}", livekit_server_id),
format!("secret-{}", livekit_server_id),
deterministic.clone(),
)
.unwrap();
let executor = Executor::Deterministic(deterministic.clone());
let app_state = Self::build_app_state(&test_db, &livekit_server, executor.clone()).await;
let epoch = app_state
.db
.create_server(&app_state.config.zed_environment)
.await
.unwrap();
let server = Server::new(epoch, app_state.clone());
server.start().await.unwrap();
// Advance clock to ensure the server's cleanup task is finished.
deterministic.advance_clock(CLEANUP_TIMEOUT);
Self {
app_state,
server,
connection_killers: Default::default(),
forbid_connections: Default::default(),
next_github_user_id: 0,
test_db,
test_livekit_server: livekit_server,
}
}
pub async fn start2(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) -> (TestServer, TestClient, TestClient, ChannelId) {
let mut server = Self::start(cx_a.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_id = server
.make_channel(
"test-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b)],
)
.await;
cx_a.run_until_parked();
(server, client_a, client_b, channel_id)
}
pub async fn start1(cx: &mut TestAppContext) -> (TestServer, TestClient) {
let mut server = Self::start(cx.executor().clone()).await;
let client = server.create_client(cx, "user_a").await;
(server, client)
}
pub async fn reset(&self) {
self.app_state.db.reset();
let epoch = self
.app_state
.db
.create_server(&self.app_state.config.zed_environment)
.await
.unwrap();
self.server.reset(epoch);
}
pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
const ACCESS_TOKEN: &str = "the-token";
let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
gpui_tokio::init(cx);
if cx.has_global::<SettingsStore>() {
panic!("Same cx used to create two test clients")
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
theme_settings::init(theme::LoadThemes::JustBase, cx);
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
let clock = Arc::new(FakeSystemClock::new());
let user_id = if let Ok(Some(user)) = self
.app_state
.user_service
.get_user_by_github_login(name)
.await
{
user.id
} else {
let github_user_id = self.next_github_user_id;
self.next_github_user_id += 1;
self.app_state
.user_service
.as_fake()
.create_user(
&format!("{name}@example.com"),
None,
false,
NewUserParams {
github_login: name.into(),
github_user_id,
},
)
.await
};
let http = FakeHttpClient::create({
let name = name.to_string();
move |req| {
let name = name.clone();
async move {
match (req.method(), req.uri().path()) {
(&Method::GET, "/client/users/me") => {
let credentials = parse_authorization_header(&req);
if credentials
!= Some(Credentials {
user_id: user_id.to_proto(),
access_token: ACCESS_TOKEN.into(),
})
{
return Ok(http_client::Response::builder()
.status(401)
.body("Unauthorized".into())
.unwrap());
}
Ok(http_client::Response::builder()
.status(200)
.body(
serde_json::to_string(&make_get_authenticated_user_response(
user_id.0, name,
))
.unwrap()
.into(),
)
.unwrap())
}
_ => Ok(http_client::Response::builder()
.status(404)
.body("Not Found".into())
.unwrap()),
}
}
}
});
let client_name = name.to_string();
let client = cx.update(|cx| Client::new(clock, http.clone(), cx));
let server = self.server.clone();
let user_service = self.app_state.user_service.clone();
let connection_killers = self.connection_killers.clone();
let forbid_connections = self.forbid_connections.clone();
client
.set_id(user_id.to_proto())
.override_authenticate(move |cx| {
cx.spawn(async move |_| {
Ok(Credentials {
user_id: user_id.to_proto(),
access_token: ACCESS_TOKEN.into(),
})
})
})
.override_establish_connection(move |credentials, cx| {
assert_eq!(
credentials,
&Credentials {
user_id: user_id.0 as u64,
access_token: ACCESS_TOKEN.into(),
}
);
let server = server.clone();
let user_service = user_service.clone();
let connection_killers = connection_killers.clone();
let forbid_connections = forbid_connections.clone();
let client_name = client_name.clone();
cx.spawn(async move |cx| {
if forbid_connections.load(SeqCst) {
Err(EstablishConnectionError::other(anyhow!(
"server is forbidding connections"
)))
} else {
let (client_conn, server_conn, killed) =
Connection::in_memory(cx.background_executor().clone());
let (connection_id_tx, connection_id_rx) = oneshot::channel();
let user = user_service
.as_fake()
.get_user_by_id(user_id)
.await
.map_err(|e| {
EstablishConnectionError::Other(anyhow!(
"retrieving user failed: {}",
e
))
})?
.unwrap();
cx.background_spawn(server.handle_connection(
server_conn,
client_name,
Principal::User(user),
ZedVersion(semver::Version::new(1, 0, 0)),
Some("test".to_string()),
None,
None,
None,
Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()),
None,
))
.detach();
let connection_id = connection_id_rx.await.map_err(|e| {
EstablishConnectionError::Other(anyhow!(
"{} (is server shutting down?)",
e
))
})?;
connection_killers
.lock()
.insert(connection_id.into(), killed);
Ok(client_conn)
}
})
});
let git_hosting_provider_registry = cx.update(GitHostingProviderRegistry::default_global);
git_hosting_provider_registry
.register_hosting_provider(Arc::new(git_hosting_providers::Github::public_instance()));
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
workspace_store,
languages: language_registry,
fs: fs.clone(),
build_window_options: |_, _| Default::default(),
node_runtime: NodeRuntime::unavailable(),
session,
});
let os_keymap = "keymaps/default-macos.json";
cx.update(|cx| {
theme_settings::init(theme::LoadThemes::JustBase, cx);
Project::init(&client, cx);
client::init(&client, cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
call::init(client.clone(), user_store.clone(), cx);
channel::init(&client, user_store.clone(), cx);
notifications::init(client.clone(), user_store, cx);
collab_ui::init(&app_state, cx);
file_finder::init(cx);
menu::init();
cx.bind_keys(
settings::KeymapFile::load_asset_allow_partial_failure(os_keymap, cx).unwrap(),
);
language_model::LanguageModelRegistry::test(cx);
});
client
.connect(false, &cx.to_async())
.await
.into_response()
.unwrap();
let client = TestClient {
app_state,
username: name.to_string(),
channel_store: cx.read(ChannelStore::global),
notification_store: cx.read(NotificationStore::global),
state: Default::default(),
};
client.wait_for_current_user(cx).await;
client
}
pub fn disconnect_client(&self, peer_id: PeerId) {
self.connection_killers
.lock()
.remove(&peer_id)
.unwrap()
.store(true, SeqCst);
}
pub fn simulate_long_connection_interruption(
&self,
peer_id: PeerId,
deterministic: BackgroundExecutor,
) {
self.forbid_connections();
self.disconnect_client(peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
self.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
deterministic.run_until_parked();
}
pub fn forbid_connections(&self) {
self.forbid_connections.store(true, SeqCst);
}
pub fn allow_connections(&self) {
self.forbid_connections.store(false, SeqCst);
}
pub async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
for ix in 1..clients.len() {
let (left, right) = clients.split_at_mut(ix);
let (client_a, cx_a) = left.last_mut().unwrap();
for (client_b, cx_b) in right {
client_a
.app_state
.user_store
.update(*cx_a, |store, cx| {
store.request_contact(client_b.user_id().unwrap(), cx)
})
.await
.unwrap();
cx_a.executor().run_until_parked();
client_b
.app_state
.user_store
.update(*cx_b, |store, cx| {
store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
})
.await
.unwrap();
}
}
}
pub async fn make_channel(
&self,
channel: &str,
parent: Option<ChannelId>,
admin: (&TestClient, &mut TestAppContext),
members: &mut [(&TestClient, &mut TestAppContext)],
) -> ChannelId {
let (admin_client, admin_cx) = admin;
// Subscribe to channels (simulates opening the collab panel)
admin_client.initialize_channel_store(admin_cx);
admin_cx.executor().run_until_parked();
let channel_id = admin_cx
.read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.create_channel(channel, parent, cx)
})
.await
.unwrap();
for (member_client, member_cx) in members {
// Subscribe member to channels (simulates opening the collab panel)
member_client.initialize_channel_store(member_cx);
member_cx.executor().run_until_parked();
admin_cx
.read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.invite_member(
channel_id,
member_client.user_id().unwrap(),
ChannelRole::Member,
cx,
)
})
.await
.unwrap();
admin_cx.executor().run_until_parked();
member_cx
.read(ChannelStore::global)
.update(*member_cx, |channels, cx| {
channels.respond_to_channel_invite(channel_id, true, cx)
})
.await
.unwrap();
}
channel_id
}
pub async fn make_public_channel(
&self,
channel: &str,
client: &TestClient,
cx: &mut TestAppContext,
) -> ChannelId {
let channel_id = self
.make_channel(channel, None, (client, cx), &mut [])
.await;
client
.channel_store()
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
proto::ChannelVisibility::Public,
cx,
)
})
.await
.unwrap();
channel_id
}
pub async fn make_channel_tree(
&self,
channels: &[(&str, Option<&str>)],
creator: (&TestClient, &mut TestAppContext),
) -> Vec<ChannelId> {
let mut observed_channels = HashMap::default();
let mut result = Vec::new();
for (channel, parent) in channels {
let id;
if let Some(parent) = parent {
if let Some(parent_id) = observed_channels.get(parent) {
id = self
.make_channel(channel, Some(*parent_id), (creator.0, creator.1), &mut [])
.await;
} else {
panic!(
"Edge {}->{} referenced before {} was created",
parent, channel, parent
)
}
} else {
id = self
.make_channel(channel, None, (creator.0, creator.1), &mut [])
.await;
}
observed_channels.insert(channel, id);
result.push(id);
}
result
}
pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
self.make_contacts(clients).await;
let (left, right) = clients.split_at_mut(1);
let (_client_a, cx_a) = &mut left[0];
let active_call_a = cx_a.read(ActiveCall::global);
for (client_b, cx_b) in right {
let user_id_b = client_b.current_user_id(cx_b).to_proto();
active_call_a
.update(*cx_a, |call, cx| call.invite(user_id_b, None, cx))
.await
.unwrap();
cx_b.executor().run_until_parked();
let active_call_b = cx_b.read(ActiveCall::global);
active_call_b
.update(*cx_b, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
}
}
pub async fn build_app_state(
test_db: &TestDb,
livekit_test_server: &LivekitTestServer,
executor: Executor,
) -> Arc<AppState> {
Arc::new(AppState {
db: test_db.db().clone(),
http_client: None,
livekit_client: Some(Arc::new(livekit_test_server.create_api_client())),
blob_store_client: None,
executor,
kinesis_client: None,
user_service: FakeUserService::new(test_db.db().clone()),
config: Config {
http_port: 0,
database_url: "".into(),
database_max_connections: 0,
livekit_server: None,
livekit_key: None,
livekit_secret: None,
rust_log: None,
log_json: None,
zed_environment: "test".into(),
zed_cloud_internal_api_key: "test-internal-api-key".into(),
blob_store_url: None,
blob_store_region: None,
blob_store_access_key: None,
blob_store_secret_key: None,
blob_store_bucket: None,
zed_client_checksum_seed: None,
seed_path: None,
kinesis_region: None,
kinesis_stream: None,
kinesis_access_key: None,
kinesis_secret_key: None,
},
})
}
}
impl Deref for TestServer {
type Target = Server;
fn deref(&self) -> &Self::Target {
&self.server
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.server.teardown();
self.test_livekit_server.teardown().unwrap();
}
}
impl Deref for TestClient {
type Target = Arc<Client>;
fn deref(&self) -> &Self::Target {
&self.app_state.client
}
}
impl TestClient {
pub fn fs(&self) -> Arc<FakeFs> {
self.app_state.fs.as_fake()
}
pub fn channel_store(&self) -> &Entity<ChannelStore> {
&self.channel_store
}
pub fn notification_store(&self) -> &Entity<NotificationStore> {
&self.notification_store
}
pub fn user_store(&self) -> &Entity<UserStore> {
&self.app_state.user_store
}
pub fn language_registry(&self) -> &Arc<LanguageRegistry> {
&self.app_state.languages
}
pub fn client(&self) -> &Arc<Client> {
&self.app_state.client
}
pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
UserId::from_proto(self.app_state.user_store.read_with(cx, |user_store, _| {
user_store.current_user().unwrap().legacy_id
}))
}
pub async fn wait_for_current_user(&self, cx: &TestAppContext) {
let mut authed_user = self
.app_state
.user_store
.read_with(cx, |user_store, _| user_store.watch_current_user());
while authed_user.next().await.unwrap().is_none() {}
}
pub async fn clear_contacts(&self, cx: &mut TestAppContext) {
self.app_state
.user_store
.update(cx, |store, _| store.clear_contacts())
.await;
}
/// Subscribe to channels. In production this happens when the user opens the collab panel.
pub fn initialize_channel_store(&self, cx: &mut TestAppContext) {
self.channel_store
.update(cx, |channel_store, _| channel_store.initialize());
}
pub fn local_projects(&self) -> impl Deref<Target = Vec<Entity<Project>>> + '_ {
Ref::map(self.state.borrow(), |state| &state.local_projects)
}
pub fn dev_server_projects(&self) -> impl Deref<Target = Vec<Entity<Project>>> + '_ {
Ref::map(self.state.borrow(), |state| &state.dev_server_projects)
}
pub fn local_projects_mut(&self) -> impl DerefMut<Target = Vec<Entity<Project>>> + '_ {
RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects)
}
pub fn dev_server_projects_mut(&self) -> impl DerefMut<Target = Vec<Entity<Project>>> + '_ {
RefMut::map(self.state.borrow_mut(), |state| {
&mut state.dev_server_projects
})
}
pub fn buffers_for_project<'a>(
&'a self,
project: &Entity<Project>,
) -> impl DerefMut<Target = HashSet<Entity<language::Buffer>>> + 'a {
RefMut::map(self.state.borrow_mut(), |state| {
state.buffers.entry(project.clone()).or_default()
})
}
pub fn buffers(
&self,
) -> impl DerefMut<Target = HashMap<Entity<Project>, HashSet<Entity<language::Buffer>>>> + '_
{
RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers)
}
pub fn channel_buffers(&self) -> impl DerefMut<Target = HashSet<Entity<ChannelBuffer>>> + '_ {
RefMut::map(self.state.borrow_mut(), |state| &mut state.channel_buffers)
}
pub fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
self.app_state
.user_store
.read_with(cx, |store, _| ContactsSummary {
current: store
.contacts()
.iter()
.map(|contact| contact.user.github_login.clone().to_string())
.collect(),
outgoing_requests: store
.outgoing_contact_requests()
.iter()
.map(|user| user.github_login.clone().to_string())
.collect(),
incoming_requests: store
.incoming_contact_requests()
.iter()
.map(|user| user.github_login.clone().to_string())
.collect(),
})
}
pub async fn build_local_project(
&self,
root_path: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> (Entity<Project>, WorktreeId) {
let project = self.build_empty_local_project(false, cx);
let (worktree, _) = project
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
.await
.unwrap();
worktree
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
cx.run_until_parked();
(project, worktree.read_with(cx, |tree, _| tree.id()))
}
pub async fn build_local_project_with_trust(
&self,
root_path: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> (Entity<Project>, WorktreeId) {
let project = self.build_empty_local_project(true, cx);
let (worktree, _) = project
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
.await
.unwrap();
worktree
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
cx.run_until_parked();
(project, worktree.read_with(cx, |tree, _| tree.id()))
}
pub async fn build_ssh_project(
&self,
root_path: impl AsRef<Path>,
ssh: Entity<RemoteClient>,
init_worktree_trust: bool,
cx: &mut TestAppContext,
) -> (Entity<Project>, WorktreeId) {
let project = cx.update(|cx| {
Project::remote(
ssh,
self.client().clone(),
self.app_state.node_runtime.clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.fs.clone(),
init_worktree_trust,
cx,
)
});
let (worktree, _) = project
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
.await
.unwrap();
(project, worktree.read_with(cx, |tree, _| tree.id()))
}
pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Entity<Project> {
self.fs()
.insert_tree(
path!("/a"),
json!({
"1.txt": "one\none\none",
"2.js": "function two() { return 2; }",
"3.rs": "mod test",
}),
)
.await;
self.build_local_project(path!("/a"), cx).await.0
}
pub async fn host_workspace(
&self,
workspace: &Entity<Workspace>,
channel_id: ChannelId,
cx: &mut VisualTestContext,
) {
cx.update(|_, cx| {
let active_call = ActiveCall::global(cx);
active_call.update(cx, |call, cx| call.join_channel(channel_id, cx))
})
.await
.unwrap();
cx.update(|_, cx| {
let active_call = ActiveCall::global(cx);
let project = workspace.read(cx).project().clone();
active_call.update(cx, |call, cx| call.share_project(project, cx))
})
.await
.unwrap();
cx.executor().run_until_parked();
}
pub async fn join_workspace<'a>(
&'a self,
channel_id: ChannelId,
cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, None, cx))
.await
.unwrap();
cx.run_until_parked();
self.active_workspace(cx)
}
pub fn build_empty_local_project(
&self,
init_worktree_trust: bool,
cx: &mut TestAppContext,
) -> Entity<Project> {
cx.update(|cx| {
Project::local(
self.client().clone(),
self.app_state.node_runtime.clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.fs.clone(),
None,
project::LocalProjectFlags {
init_worktree_trust,
..Default::default()
},
cx,
)
})
}
pub async fn join_remote_project(
&self,
host_project_id: u64,
guest_cx: &mut TestAppContext,
) -> Entity<Project> {
let active_call = guest_cx.read(ActiveCall::global);
let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone());
room.update(guest_cx, |room, cx| {
room.join_project(
host_project_id,
self.app_state.languages.clone(),
self.app_state.fs.clone(),
cx,
)
})
.await
.unwrap()
}
pub fn build_workspace<'a>(
&'a self,
project: &Entity<Project>,
cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
let app_state = self.app_state.clone();
let project = project.clone();
let window = cx.add_window(|window, cx| {
window.activate_window();
let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
MultiWorkspace::new(workspace, window, cx)
});
let cx = VisualTestContext::from_window(*window, cx).into_mut();
cx.run_until_parked();
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
(workspace, cx)
}
pub async fn build_test_workspace<'a>(
&'a self,
cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
let project = self.build_test_project(cx).await;
let app_state = self.app_state.clone();
let window = cx.add_window(|window, cx| {
window.activate_window();
let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
MultiWorkspace::new(workspace, window, cx)
});
let cx = VisualTestContext::from_window(*window, cx).into_mut();
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
(workspace, cx)
}
pub fn active_workspace<'a>(
&'a self,
cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
let window = cx.update(|cx| {
cx.active_window()
.unwrap()
.downcast::<MultiWorkspace>()
.unwrap()
});
let entity = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut();
// it might be nice to try and cleanup these at the end of each test.
(entity, cx)
}
}
pub fn open_channel_notes(
channel_id: ChannelId,
cx: &mut VisualTestContext,
) -> Task<anyhow::Result<Entity<ChannelView>>> {
let window = cx.update(|_, cx| {
cx.active_window()
.unwrap()
.downcast::<MultiWorkspace>()
.unwrap()
});
let entity = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
cx.update(|window, cx| ChannelView::open(channel_id, None, entity.clone(), window, cx))
}
impl Drop for TestClient {
fn drop(&mut self) {
self.app_state.client.teardown();
}
}