From aaebee488f18257b0262d2eb86b027ba4f5421eb Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 9 Jan 2026 13:45:10 +0100 Subject: [PATCH] Add `recent_projects` remote test (#46436) Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 4 + .../remote_editing_collaboration_tests.rs | 24 ++-- crates/recent_projects/Cargo.toml | 8 +- .../recent_projects/src/remote_connections.rs | 105 +++++++++++++++++- crates/remote/src/remote_client.rs | 41 ++++--- crates/remote/src/transport/mock.rs | 30 +++-- .../remote_server/src/remote_editing_tests.rs | 4 +- 7 files changed, 173 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80d0f518ffd14f505e338b30f876a31ef9808a19..0ce59d35561f574c20b00e29cd2b221c78e7fa1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13297,11 +13297,14 @@ dependencies = [ "dap", "db", "editor", + "extension", "extension_host", "file_finder", + "fs", "futures 0.3.31", "fuzzy", "gpui", + "http_client", "indoc", "language", "log", @@ -13314,6 +13317,7 @@ dependencies = [ "project", "release_channel", "remote", + "remote_server", "semver", "serde", "serde_json", diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 4ac186d0c9f687a0a5483be88a306f1d2120285b..9b67e092e97cc95c040a153cb7ab14e3afd03263 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -62,7 +62,7 @@ async fn test_sharing_an_ssh_remote_project( .await; // Set up project on remote FS - let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -104,7 +104,7 @@ async fn test_sharing_an_ssh_remote_project( ) }); - let client_ssh = RemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::connect_mock(opts, cx_a).await; let (project_a, worktree_id) = client_a .build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a) .await; @@ -232,7 +232,7 @@ async fn test_ssh_collaboration_git_branches( .await; // Set up project on remote FS - let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree("/project", serde_json::json!({ ".git":{} })) @@ -265,7 +265,7 @@ async fn test_ssh_collaboration_git_branches( ) }); - let client_ssh = RemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::connect_mock(opts, cx_a).await; let (project_a, _) = client_a .build_ssh_project("/project", client_ssh, false, cx_a) .await; @@ -417,7 +417,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); let buffer_text = "let one = \"two\""; let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; @@ -470,7 +470,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( ) }); - let client_ssh = RemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::connect_mock(opts, cx_a).await; let (project_a, worktree_id) = client_a .build_ssh_project(path!("/project"), client_ssh, false, cx_a) .await; @@ -601,7 +601,7 @@ async fn test_remote_server_debugger( release_channel::init(semver::Version::new(0, 0, 0), cx); dap_adapters::init(cx); }); - let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -632,7 +632,7 @@ async fn test_remote_server_debugger( ) }); - let client_ssh = RemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::connect_mock(opts, cx_a).await; let mut server = TestServer::start(server_cx.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; cx_a.update(|cx| { @@ -710,7 +710,7 @@ async fn test_slow_adapter_startup_retries( release_channel::init(semver::Version::new(0, 0, 0), cx); dap_adapters::init(cx); }); - let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -741,7 +741,7 @@ async fn test_slow_adapter_startup_retries( ) }); - let client_ssh = RemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::connect_mock(opts, cx_a).await; let mut server = TestServer::start(server_cx.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; cx_a.update(|cx| { @@ -870,7 +870,7 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m let server_name = "override-rust-analyzer"; let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0)); - let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -944,7 +944,7 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m ) }); - let client_ssh = RemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::connect_mock(opts, cx_a).await; let (project_a, worktree_id_a) = client_a .build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a) .await; diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 67d9594bbf48b7740a97fe18c32e92fba19ce5e5..777b3c42de252b707ca6e28af007f9ceb206fb38 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -59,9 +59,15 @@ windows-registry = "0.6.0" [dev-dependencies] dap.workspace = true editor = { workspace = true, features = ["test-support"] } +extension.workspace = true +fs.workspace = true +gpui = { workspace = true, features = ["test-support"] } +http_client.workspace = true language = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } +release_channel.workspace = true +remote = { workspace = true, features = ["test-support"] } +remote_server.workspace = true serde_json.workspace = true settings = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } -remote = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 888ac26ce011da8fa1a36757f26db27ffd575d85..db8f743483f86fc8df56c7775decf8582f5cd24b 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -848,8 +848,10 @@ pub async fn open_remote_project( window .update(cx, |workspace, _, cx| { if let Some(client) = workspace.project().read(cx).remote_client() { - ExtensionStore::global(cx) - .update(cx, |store, cx| store.register_remote_client(client, cx)); + if let Some(extension_store) = ExtensionStore::try_global(cx) { + extension_store + .update(cx, |store, cx| store.register_remote_client(client, cx)); + } } }) .ok(); @@ -901,3 +903,102 @@ async fn path_exists(connection: &Arc, path: &Path) -> boo }; child.status().await.is_ok_and(|status| status.success()) } + +#[cfg(test)] +mod tests { + use super::*; + use extension::ExtensionHostProxy; + use fs::FakeFs; + use gpui::TestAppContext; + use http_client::BlockedHttpClient; + use node_runtime::NodeRuntime; + use remote::RemoteClient; + use remote_server::{HeadlessAppState, HeadlessProject}; + use serde_json::json; + use util::path; + + #[gpui::test] + async fn test_open_remote_project_with_mock_connection( + cx: &mut TestAppContext, + server_cx: &mut TestAppContext, + ) { + let app_state = init_test(cx); + let executor = cx.executor(); + + cx.update(|cx| { + release_channel::init(semver::Version::new(0, 0, 0), cx); + }); + server_cx.update(|cx| { + release_channel::init(semver::Version::new(0, 0, 0), cx); + }); + + let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx); + + let remote_fs = FakeFs::new(server_cx.executor()); + remote_fs + .insert_tree( + path!("/project"), + json!({ + "src": { + "main.rs": "fn main() {}", + }, + "README.md": "# Test Project", + }), + ) + .await; + + server_cx.update(HeadlessProject::init); + let http_client = Arc::new(BlockedHttpClient); + let node_runtime = NodeRuntime::unavailable(); + let languages = Arc::new(language::LanguageRegistry::new(server_cx.executor())); + let proxy = Arc::new(ExtensionHostProxy::new()); + + let _headless = server_cx.new(|cx| { + HeadlessProject::new( + HeadlessAppState { + session: server_session, + fs: remote_fs.clone(), + http_client, + node_runtime, + languages, + extension_host_proxy: proxy, + }, + false, + cx, + ) + }); + + drop(connect_guard); + + let paths = vec![PathBuf::from(path!("/project"))]; + let open_options = workspace::OpenOptions::default(); + + let mut async_cx = cx.to_async(); + let result = open_remote_project(opts, paths, app_state, open_options, &mut async_cx).await; + + executor.run_until_parked(); + + assert!(result.is_ok(), "open_remote_project should succeed"); + + let windows = cx.update(|cx| cx.windows().len()); + assert_eq!(windows, 1, "Should have opened a window"); + + let workspace_handle = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + + workspace_handle + .update(cx, |workspace, _, cx| { + let project = workspace.project().read(cx); + assert!(project.is_remote(), "Project should be a remote project"); + }) + .unwrap(); + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let state = AppState::test(cx); + crate::init(cx); + editor::init(cx); + state + }) + } +} diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 41394783946349922d1de2a63034387688dd1dee..c41b168b7126c340b6536f9bb55bf7e3f839c5c5 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -1,3 +1,5 @@ +#[cfg(any(test, feature = "test-support"))] +use crate::transport::mock::ConnectGuard; use crate::{ SshConnectionOptions, protocol::MessageId, @@ -977,23 +979,26 @@ impl RemoteClient { /// Creates a mock connection pair for testing. /// /// This is the recommended way to create mock remote connections for tests. - /// It returns both the `MockConnectionOptions` (which can be passed to create - /// a `HeadlessProject`) and an `AnyProtoClient` for the server side. + /// It returns the `MockConnectionOptions` (which can be passed to create a + /// `HeadlessProject`), an `AnyProtoClient` for the server side and a + /// `ConnectGuard` for the client side which blocks the connection from + /// being established until dropped. /// /// # Example /// ```ignore - /// let (opts, server_session) = RemoteClient::fake_server(cx, server_cx); + /// let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx); /// // Set up HeadlessProject with server_session... + /// drop(connect_guard); /// let client = RemoteClient::fake_client(opts, cx).await; /// ``` #[cfg(any(test, feature = "test-support"))] pub fn fake_server( client_cx: &mut gpui::TestAppContext, server_cx: &mut gpui::TestAppContext, - ) -> (RemoteConnectionOptions, AnyProtoClient) { + ) -> (RemoteConnectionOptions, AnyProtoClient, ConnectGuard) { use crate::transport::mock::MockConnection; - let (opts, server_client) = MockConnection::new(client_cx, server_cx); - (opts.into(), server_client) + let (opts, server_client, connect_guard) = MockConnection::new(client_cx, server_cx); + (opts.into(), server_client, connect_guard) } /// Creates a `RemoteClient` connected to a mock server. @@ -1002,10 +1007,11 @@ impl RemoteClient { /// `HeadlessProject` with the server session, then call this method /// to create the client. #[cfg(any(test, feature = "test-support"))] - pub async fn fake_client( + pub async fn connect_mock( opts: RemoteConnectionOptions, client_cx: &mut gpui::TestAppContext, ) -> Entity { + assert!(matches!(opts, RemoteConnectionOptions::Mock(..))); use crate::transport::mock::MockDelegate; let (_tx, rx) = oneshot::channel(); let mut cx = client_cx.to_async(); @@ -1047,7 +1053,7 @@ struct ConnectionPool { impl Global for ConnectionPool {} impl ConnectionPool { - pub fn connect( + fn connect( &mut self, opts: RemoteConnectionOptions, delegate: Arc, @@ -1095,16 +1101,15 @@ impl ConnectionPool { .map(|connection| Arc::new(connection) as Arc) } #[cfg(any(test, feature = "test-support"))] - RemoteConnectionOptions::Mock(opts) => { - cx.update(|cx| { - cx.default_global::() - .take(&opts) - .ok_or_else(|| anyhow!( - "Mock connection not found. Call MockConnection::new() first." - )) - .map(|connection| connection as Arc) - }) - } + RemoteConnectionOptions::Mock(opts) => match cx.update(|cx| { + cx.default_global::() + .take(&opts) + }) { + Some(connection) => Ok(connection.await as Arc), + None => Err(anyhow!( + "Mock connection not found. Call MockConnection::new() first." + )), + }, }; cx.update_global(|pool: &mut Self, _| { diff --git a/crates/remote/src/transport/mock.rs b/crates/remote/src/transport/mock.rs index 5d392fd685903632fd786b4a95cf737e71e517dd..01e74c2963e5115ace0c19825de86b5cd6cfb5ad 100644 --- a/crates/remote/src/transport/mock.rs +++ b/crates/remote/src/transport/mock.rs @@ -37,7 +37,10 @@ use async_trait::async_trait; use collections::HashMap; use futures::{ FutureExt, SinkExt, StreamExt, - channel::mpsc::{self, Sender}, + channel::{ + mpsc::{self, Sender}, + oneshot, + }, select_biased, }; use gpui::{App, AppContext as _, AsyncApp, Global, Task, TestAppContext}; @@ -94,21 +97,30 @@ unsafe impl Sync for SendableCx {} /// it retrieves the connection from this registry. #[derive(Default)] pub struct MockConnectionRegistry { - pending: HashMap>, + pending: HashMap, Arc)>, } impl Global for MockConnectionRegistry {} impl MockConnectionRegistry { /// Called by `ConnectionPool::connect` to retrieve a pre-registered mock connection. - pub fn take(&mut self, opts: &MockConnectionOptions) -> Option> { - self.pending.remove(opts) + pub fn take( + &mut self, + opts: &MockConnectionOptions, + ) -> Option> + use<>> { + let (guard, con) = self.pending.remove(opts)?; + Some(async move { + _ = guard.await; + con + }) } } /// Helper for creating mock connection pairs in tests. pub struct MockConnection; +pub type ConnectGuard = oneshot::Sender<()>; + impl MockConnection { /// Creates a new mock connection pair for testing. /// @@ -125,10 +137,10 @@ impl MockConnection { /// # Arguments /// - `client_cx`: The test context for the client side /// - `server_cx`: The test context for the server/headless side - pub fn new( + pub(crate) fn new( client_cx: &mut TestAppContext, server_cx: &mut TestAppContext, - ) -> (MockConnectionOptions, AnyProtoClient) { + ) -> (MockConnectionOptions, AnyProtoClient, ConnectGuard) { static NEXT_ID: AtomicU64 = AtomicU64::new(0); let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); let opts = MockConnectionOptions { id }; @@ -144,13 +156,15 @@ impl MockConnection { server_cx: SendableCx::new(server_cx), }); + let (tx, rx) = oneshot::channel(); + client_cx.update(|cx| { cx.default_global::() .pending - .insert(opts.clone(), connection); + .insert(opts.clone(), (rx, connection)); }); - (opts, server_client.into()) + (opts, server_client.into(), tx) } } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 801e488f4b669d9a5b0d4c6b2b5a1f7728f2b8a4..fa140aeef46caae32031a912a950a45619bcafe0 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1920,7 +1920,7 @@ pub async fn init_test( }); init_logger(); - let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx); + let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx); let http_client = Arc::new(BlockedHttpClient); let node_runtime = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(cx.executor())); @@ -1941,7 +1941,7 @@ pub async fn init_test( ) }); - let ssh = RemoteClient::fake_client(opts, cx).await; + let ssh = RemoteClient::connect_mock(opts, cx).await; let project = build_project(ssh, cx); project .update(cx, {