Cargo.lock 🔗
@@ -3372,6 +3372,7 @@ dependencies = [
"text",
"theme",
"time",
+ "title_bar",
"tokio",
"toml 0.8.23",
"tower 0.4.13",
Lukas Wirth created
Release Notes:
- N/A *or* Added/Fixed/Improved ...
Cargo.lock | 1
crates/collab/Cargo.toml | 1
crates/collab/src/tests/remote_editing_collaboration_tests.rs | 2
crates/collab_ui/Cargo.toml | 1
crates/dap/src/client.rs | 4
crates/dap/src/transport.rs | 5
crates/git_ui/Cargo.toml | 2
crates/project/Cargo.toml | 2
crates/project/src/trusted_worktrees.rs | 4
crates/recent_projects/Cargo.toml | 5
crates/recent_projects/src/recent_projects.rs | 2
crates/recent_projects/src/remote_connections.rs | 12
crates/recent_projects/src/remote_servers.rs | 5
crates/remote/src/remote.rs | 5
crates/remote/src/remote_client.rs | 303 ---
crates/remote/src/transport.rs | 2
crates/remote/src/transport/mock.rs | 312 +++++
crates/remote_server/src/remote_editing_tests.rs | 2
crates/title_bar/Cargo.toml | 2
crates/title_bar/src/title_bar.rs | 2
crates/workspace/src/persistence.rs | 5
crates/zed/Cargo.toml | 1
22 files changed, 426 insertions(+), 254 deletions(-)
@@ -3372,6 +3372,7 @@ dependencies = [
"text",
"theme",
"time",
+ "title_bar",
"tokio",
"toml 0.8.23",
"tower 0.4.13",
@@ -125,6 +125,7 @@ smol.workspace = true
sqlx = { version = "0.8", features = ["sqlite"] }
task.workspace = true
theme.workspace = true
+title_bar = { workspace = true, features = ["test-support"] }
unindent.workspace = true
util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
@@ -676,7 +676,7 @@ async fn test_remote_server_debugger(
});
session.update(cx_a, |session, _| {
- assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
+ assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
});
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
@@ -26,6 +26,7 @@ test-support = [
"util/test-support",
"workspace/test-support",
"http_client/test-support",
+ "title_bar/test-support",
]
[dependencies]
@@ -59,7 +59,9 @@ impl DebugAdapterClient {
pub fn should_reconnect_for_ssh(&self) -> bool {
self.transport_delegate.tcp_arguments().is_some()
- && self.binary.command.as_deref() == Some("ssh")
+ && (self.binary.command.as_deref() == Some("ssh")
+ || (cfg!(feature = "test-support")
+ && self.binary.command.as_deref() == Some("mock")))
}
pub async fn connect(
@@ -221,10 +221,7 @@ impl TransportDelegate {
}));
}
- {
- let mut lock = self.server_tx.lock().await;
- *lock = Some(server_tx.clone());
- }
+ *self.server_tx.lock().await = Some(server_tx.clone());
Ok(())
}
@@ -13,7 +13,7 @@ name = "git_ui"
path = "src/git_ui.rs"
[features]
-test-support = ["multi_buffer/test-support"]
+test-support = ["multi_buffer/test-support", "recent_projects/test-support"]
[dependencies]
agent_settings.workspace = true
@@ -17,6 +17,7 @@ test-support = [
"buffer_diff/test-support",
"client/test-support",
"language/test-support",
+ "remote/test-support",
"settings/test-support",
"snippet_provider/test-support",
"text/test-support",
@@ -117,6 +118,7 @@ snippet_provider = { workspace = true, features = ["test-support"] }
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }
+remote = { workspace = true, features = ["test-support"] }
[package.metadata.cargo-machete]
ignored = ["tracing"]
@@ -162,6 +162,10 @@ impl From<RemoteConnectionOptions> for RemoteHostLocation {
Some(SharedString::new(docker_connection_options.name)),
SharedString::new(docker_connection_options.container_id),
),
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(mock) => {
+ (None, SharedString::new(format!("mock-{}", mock.id)))
+ }
};
Self {
user_name,
@@ -12,6 +12,10 @@ workspace = true
path = "src/recent_projects.rs"
doctest = false
+[features]
+default = []
+test-support = ["remote/test-support"]
+
[dependencies]
anyhow.workspace = true
askpass.workspace = true
@@ -60,3 +64,4 @@ project = { workspace = true, features = ["test-support"] }
serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
+remote = { workspace = true, features = ["test-support"] }
@@ -719,6 +719,8 @@ impl PickerDelegate for RecentProjectsDelegate {
RemoteConnectionOptions::Ssh { .. } => IconName::Server,
RemoteConnectionOptions::Wsl { .. } => IconName::Linux,
RemoteConnectionOptions::Docker(_) => IconName::Box,
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(_) => IconName::Server,
})
.color(Color::Muted)
.into_any_element()
@@ -309,6 +309,10 @@ impl RemoteConnectionModal {
(options.distro_name.clone(), None, true, false)
}
RemoteConnectionOptions::Docker(options) => (options.name.clone(), None, false, true),
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(options) => {
+ (format!("mock-{}", options.id), None, false, false)
+ }
};
Self {
prompt: cx.new(|cx| {
@@ -720,6 +724,10 @@ pub async fn open_remote_project(
RemoteConnectionOptions::Docker(_) => {
"Failed to connect to Dev Container"
}
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(_) => {
+ "Failed to connect to mock server"
+ }
},
Some(&format!("{e:#}")),
&["Retry", "Cancel"],
@@ -779,6 +787,10 @@ pub async fn open_remote_project(
RemoteConnectionOptions::Docker(_) => {
"Failed to connect to Dev Container"
}
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(_) => {
+ "Failed to connect to mock server"
+ }
},
Some(&format!("{e:#}")),
&["Retry", "Cancel"],
@@ -245,6 +245,11 @@ impl ProjectPicker {
connection_string: "".into(),
nickname: None,
},
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(options) => ProjectPickerData::Ssh {
+ connection_string: format!("mock-{}", options.id).into(),
+ nickname: None,
+ },
};
let _path_task = cx
.spawn_in(window, {
@@ -14,3 +14,8 @@ pub use remote_client::{
pub use transport::docker::DockerConnectionOptions;
pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
pub use transport::wsl::WslConnectionOptions;
+
+#[cfg(any(test, feature = "test-support"))]
+pub use transport::mock::{
+ MockConnection, MockConnectionOptions, MockConnectionRegistry, MockDelegate,
+};
@@ -958,71 +958,58 @@ impl RemoteClient {
pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> {
let opts = self.connection_options();
client_cx.spawn(async move |cx| {
- let connection = cx
- .update_global(|c: &mut ConnectionPool, _| {
- if let Some(ConnectionPoolEntry::Connecting(c)) = c.connections.get(&opts) {
- c.clone()
+ let connection = cx.update_global(|c: &mut ConnectionPool, _| {
+ if let Some(ConnectionPoolEntry::Connected(c)) = c.connections.get(&opts) {
+ if let Some(connection) = c.upgrade() {
+ connection
} else {
- panic!("missing test connection")
+ panic!("connection was dropped")
}
- })
- .await
- .unwrap();
+ } else {
+ panic!("missing test connection")
+ }
+ });
connection.simulate_disconnect(cx);
})
}
+ /// 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.
+ ///
+ /// # Example
+ /// ```ignore
+ /// let (opts, server_session) = RemoteClient::fake_server(cx, server_cx);
+ /// // Set up HeadlessProject with server_session...
+ /// 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) {
- use crate::transport::ssh::SshConnectionHost;
-
- let port = client_cx
- .update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1);
- let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: SshConnectionHost::from("<fake>".to_string()),
- port: Some(port),
- ..Default::default()
- });
- let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
- let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
- let server_client = server_cx
- .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server", false));
- let connection: Arc<dyn RemoteConnection> = Arc::new(fake::FakeRemoteConnection {
- connection_options: opts.clone(),
- server_cx: fake::SendableCx::new(server_cx),
- server_channel: server_client.clone(),
- });
-
- client_cx.update(|cx| {
- cx.update_default_global(|c: &mut ConnectionPool, cx| {
- c.connections.insert(
- opts.clone(),
- ConnectionPoolEntry::Connecting(
- cx.background_spawn({
- let connection = connection.clone();
- async move { Ok(connection.clone()) }
- })
- .shared(),
- ),
- );
- })
- });
-
- (opts, server_client.into())
+ use crate::transport::mock::MockConnection;
+ let (opts, server_client) = MockConnection::new(client_cx, server_cx);
+ (opts.into(), server_client)
}
+ /// Creates a `RemoteClient` connected to a mock server.
+ ///
+ /// Call `fake_server` first to get the connection options, set up the
+ /// `HeadlessProject` with the server session, then call this method
+ /// to create the client.
#[cfg(any(test, feature = "test-support"))]
pub async fn fake_client(
opts: RemoteConnectionOptions,
client_cx: &mut gpui::TestAppContext,
) -> Entity<Self> {
+ use crate::transport::mock::MockDelegate;
let (_tx, rx) = oneshot::channel();
let mut cx = client_cx.to_async();
- let connection = connect(opts, Arc::new(fake::Delegate), &mut cx)
+ let connection = connect(opts, Arc::new(MockDelegate), &mut cx)
.await
.unwrap();
client_cx
@@ -1031,7 +1018,7 @@ impl RemoteClient {
ConnectionIdentifier::setup(),
connection,
rx,
- Arc::new(fake::Delegate),
+ Arc::new(MockDelegate),
cx,
)
})
@@ -1107,6 +1094,17 @@ impl ConnectionPool {
.await
.map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>)
}
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(opts) => {
+ cx.update(|cx| {
+ cx.default_global::<crate::transport::mock::MockConnectionRegistry>()
+ .take(&opts)
+ .ok_or_else(|| anyhow!(
+ "Mock connection not found. Call MockConnection::new() first."
+ ))
+ .map(|connection| connection as Arc<dyn RemoteConnection>)
+ })
+ }
};
cx.update_global(|pool: &mut Self, _| {
@@ -1143,6 +1141,8 @@ pub enum RemoteConnectionOptions {
Ssh(SshConnectionOptions),
Wsl(WslConnectionOptions),
Docker(DockerConnectionOptions),
+ #[cfg(any(test, feature = "test-support"))]
+ Mock(crate::transport::mock::MockConnectionOptions),
}
impl RemoteConnectionOptions {
@@ -1151,6 +1151,8 @@ impl RemoteConnectionOptions {
RemoteConnectionOptions::Ssh(opts) => opts.host.to_string(),
RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(),
RemoteConnectionOptions::Docker(opts) => opts.name.clone(),
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(opts) => format!("mock-{}", opts.id),
}
}
}
@@ -1167,6 +1169,13 @@ impl From<WslConnectionOptions> for RemoteConnectionOptions {
}
}
+#[cfg(any(test, feature = "test-support"))]
+impl From<crate::transport::mock::MockConnectionOptions> for RemoteConnectionOptions {
+ fn from(opts: crate::transport::mock::MockConnectionOptions) -> Self {
+ RemoteConnectionOptions::Mock(opts)
+ }
+}
+
#[cfg(target_os = "windows")]
/// Open a wsl path (\\wsl.localhost\<distro>\path)
#[derive(Debug, Clone, PartialEq, Eq, gpui::Action)]
@@ -1254,7 +1263,7 @@ impl<T: Send + Clone + 'static> Signal<T> {
}
}
-struct ChannelClient {
+pub(crate) struct ChannelClient {
next_message_id: AtomicU32,
outgoing_tx: Mutex<mpsc::UnboundedSender<Envelope>>,
buffer: Mutex<VecDeque<Envelope>>,
@@ -1268,7 +1277,7 @@ struct ChannelClient {
}
impl ChannelClient {
- fn new(
+ pub(crate) fn new(
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
outgoing_tx: mpsc::UnboundedSender<Envelope>,
cx: &App,
@@ -1419,7 +1428,7 @@ impl ChannelClient {
})
}
- fn reconnect(
+ pub(crate) fn reconnect(
self: &Arc<Self>,
incoming_rx: UnboundedReceiver<Envelope>,
outgoing_tx: UnboundedSender<Envelope>,
@@ -1573,201 +1582,3 @@ impl ProtoClient for ChannelClient {
self.has_wsl_interop
}
}
-
-#[cfg(any(test, feature = "test-support"))]
-mod fake {
- use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform};
- use crate::remote_client::{CommandTemplate, RemoteConnectionOptions};
- use anyhow::Result;
- use askpass::EncryptedPassword;
- use async_trait::async_trait;
- use collections::HashMap;
- use futures::{
- FutureExt, SinkExt, StreamExt,
- channel::{
- mpsc::{self, Sender},
- oneshot,
- },
- select_biased,
- };
- use gpui::{App, AppContext as _, AsyncApp, Task, TestAppContext};
- use release_channel::ReleaseChannel;
- use rpc::proto::Envelope;
- use semver::Version;
- use std::{path::PathBuf, sync::Arc};
- use util::paths::{PathStyle, RemotePathBuf};
-
- pub(super) struct FakeRemoteConnection {
- pub(super) connection_options: RemoteConnectionOptions,
- pub(super) server_channel: Arc<ChannelClient>,
- pub(super) server_cx: SendableCx,
- }
-
- pub(super) struct SendableCx(AsyncApp);
- impl SendableCx {
- // SAFETY: When run in test mode, GPUI is always single threaded.
- pub(super) fn new(cx: &TestAppContext) -> Self {
- Self(cx.to_async())
- }
-
- // SAFETY: Enforce that we're on the main thread by requiring a valid AsyncApp
- fn get(&self, _: &AsyncApp) -> AsyncApp {
- self.0.clone()
- }
- }
-
- // SAFETY: There is no way to access a SendableCx from a different thread, see [`SendableCx::new`] and [`SendableCx::get`]
- unsafe impl Send for SendableCx {}
- unsafe impl Sync for SendableCx {}
-
- #[async_trait(?Send)]
- impl RemoteConnection for FakeRemoteConnection {
- async fn kill(&self) -> Result<()> {
- Ok(())
- }
-
- fn has_been_killed(&self) -> bool {
- false
- }
-
- fn build_command(
- &self,
- program: Option<String>,
- args: &[String],
- env: &HashMap<String, String>,
- _: Option<String>,
- _: Option<(u16, String, u16)>,
- ) -> Result<CommandTemplate> {
- let ssh_program = program.unwrap_or_else(|| "sh".to_string());
- let mut ssh_args = Vec::new();
- ssh_args.push(ssh_program);
- ssh_args.extend(args.iter().cloned());
- Ok(CommandTemplate {
- program: "ssh".into(),
- args: ssh_args,
- env: env.clone(),
- })
- }
-
- fn build_forward_ports_command(
- &self,
- forwards: Vec<(u16, String, u16)>,
- ) -> anyhow::Result<CommandTemplate> {
- Ok(CommandTemplate {
- program: "ssh".into(),
- args: std::iter::once("-N".to_owned())
- .chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
- format!("{local_port}:{host}:{remote_port}")
- }))
- .collect(),
- env: Default::default(),
- })
- }
-
- fn upload_directory(
- &self,
- _src_path: PathBuf,
- _dest_path: RemotePathBuf,
- _cx: &App,
- ) -> Task<Result<()>> {
- unreachable!()
- }
-
- fn connection_options(&self) -> RemoteConnectionOptions {
- self.connection_options.clone()
- }
-
- fn simulate_disconnect(&self, cx: &AsyncApp) {
- let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
- let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
- self.server_channel
- .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));
- }
-
- fn start_proxy(
- &self,
- _unique_identifier: String,
- _reconnect: bool,
- mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
- mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
- mut connection_activity_tx: Sender<()>,
- _delegate: Arc<dyn RemoteClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Task<Result<i32>> {
- let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
- let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
-
- self.server_channel.reconnect(
- server_incoming_rx,
- server_outgoing_tx,
- &self.server_cx.get(cx),
- );
-
- cx.background_spawn(async move {
- loop {
- select_biased! {
- server_to_client = server_outgoing_rx.next().fuse() => {
- let Some(server_to_client) = server_to_client else {
- return Ok(1)
- };
- connection_activity_tx.try_send(()).ok();
- client_incoming_tx.send(server_to_client).await.ok();
- }
- client_to_server = client_outgoing_rx.next().fuse() => {
- let Some(client_to_server) = client_to_server else {
- return Ok(1)
- };
- server_incoming_tx.send(client_to_server).await.ok();
- }
- }
- }
- })
- }
-
- fn path_style(&self) -> PathStyle {
- PathStyle::local()
- }
-
- fn shell(&self) -> String {
- "sh".to_owned()
- }
-
- fn default_system_shell(&self) -> String {
- "sh".to_owned()
- }
-
- fn has_wsl_interop(&self) -> bool {
- false
- }
- }
-
- pub(super) struct Delegate;
-
- impl RemoteClientDelegate for Delegate {
- fn ask_password(&self, _: String, _: oneshot::Sender<EncryptedPassword>, _: &mut AsyncApp) {
- unreachable!()
- }
-
- fn download_server_binary_locally(
- &self,
- _: RemotePlatform,
- _: ReleaseChannel,
- _: Option<Version>,
- _: &mut AsyncApp,
- ) -> Task<Result<PathBuf>> {
- unreachable!()
- }
-
- fn get_download_url(
- &self,
- _platform: RemotePlatform,
- _release_channel: ReleaseChannel,
- _version: Option<Version>,
- _cx: &mut AsyncApp,
- ) -> Task<Result<Option<String>>> {
- unreachable!()
- }
-
- fn set_status(&self, _: Option<&str>, _: &mut AsyncApp) {}
- }
-}
@@ -13,6 +13,8 @@ use rpc::proto::Envelope;
use smol::process::Child;
pub mod docker;
+#[cfg(any(test, feature = "test-support"))]
+pub mod mock;
pub mod ssh;
pub mod wsl;
@@ -0,0 +1,312 @@
+//! Mock transport for testing remote connections.
+//!
+//! This module provides a mock implementation of the `RemoteConnection` trait
+//! that allows testing remote editing functionality without actual SSH/WSL/Docker
+//! connections.
+//!
+//! # Usage
+//!
+//! ```rust,ignore
+//! use remote::{MockConnection, RemoteClient};
+//!
+//! #[gpui::test]
+//! async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
+//! let (opts, server_session) = MockConnection::new(cx, server_cx);
+//!
+//! // Create the headless project (server side)
+//! server_cx.update(HeadlessProject::init);
+//! let _headless = server_cx.new(|cx| {
+//! HeadlessProject::new(
+//! HeadlessAppState { session: server_session, /* ... */ },
+//! false,
+//! cx,
+//! )
+//! });
+//!
+//! // Create the client using the helper
+//! let (client, server_client) = RemoteClient::new_mock(cx, server_cx).await;
+//! // ... test logic ...
+//! }
+//! ```
+
+use crate::remote_client::{
+ ChannelClient, CommandTemplate, RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
+};
+use anyhow::Result;
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::{
+ FutureExt, SinkExt, StreamExt,
+ channel::mpsc::{self, Sender},
+ select_biased,
+};
+use gpui::{App, AppContext as _, AsyncApp, Global, Task, TestAppContext};
+use rpc::{AnyProtoClient, proto::Envelope};
+use std::{
+ path::PathBuf,
+ sync::{
+ Arc,
+ atomic::{AtomicU64, Ordering},
+ },
+};
+use util::paths::{PathStyle, RemotePathBuf};
+
+/// Unique identifier for a mock connection.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct MockConnectionOptions {
+ pub id: u64,
+}
+
+/// A mock implementation of `RemoteConnection` for testing.
+pub struct MockRemoteConnection {
+ options: MockConnectionOptions,
+ server_channel: Arc<ChannelClient>,
+ server_cx: SendableCx,
+}
+
+/// Wrapper to pass `AsyncApp` across thread boundaries in tests.
+///
+/// # Safety
+///
+/// This is safe because in test mode, GPUI is always single-threaded and so
+/// having access to one async app means being on the same main thread.
+pub(crate) struct SendableCx(AsyncApp);
+
+impl SendableCx {
+ pub(crate) fn new(cx: &TestAppContext) -> Self {
+ Self(cx.to_async())
+ }
+
+ pub(crate) fn get(&self, _: &AsyncApp) -> AsyncApp {
+ self.0.clone()
+ }
+}
+
+// SAFETY: In test mode, GPUI is always single-threaded, and SendableCx
+// is only accessed from the main thread via the get() method which
+// requires a valid AsyncApp reference.
+unsafe impl Send for SendableCx {}
+unsafe impl Sync for SendableCx {}
+
+/// Global registry that holds pre-created mock connections.
+///
+/// When `ConnectionPool::connect` is called with `MockConnectionOptions`,
+/// it retrieves the connection from this registry.
+#[derive(Default)]
+pub struct MockConnectionRegistry {
+ pending: HashMap<MockConnectionOptions, Arc<MockRemoteConnection>>,
+}
+
+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<Arc<MockRemoteConnection>> {
+ self.pending.remove(opts)
+ }
+}
+
+/// Helper for creating mock connection pairs in tests.
+pub struct MockConnection;
+
+impl MockConnection {
+ /// Creates a new mock connection pair for testing.
+ ///
+ /// This function:
+ /// 1. Creates a unique `MockConnectionOptions` identifier
+ /// 2. Sets up the server-side channel (returned as `AnyProtoClient`)
+ /// 3. Creates a `MockRemoteConnection` and registers it in the global registry
+ /// 4. The connection will be retrieved from the registry when `ConnectionPool::connect` is called
+ ///
+ /// Returns:
+ /// - `MockConnectionOptions` to pass to `remote::connect()` or `RemoteClient` creation
+ /// - `AnyProtoClient` to pass to `HeadlessProject::new()` as the session
+ ///
+ /// # Arguments
+ /// - `client_cx`: The test context for the client side
+ /// - `server_cx`: The test context for the server/headless side
+ pub fn new(
+ client_cx: &mut TestAppContext,
+ server_cx: &mut TestAppContext,
+ ) -> (MockConnectionOptions, AnyProtoClient) {
+ static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+ let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
+ let opts = MockConnectionOptions { id };
+
+ let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
+ let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
+ let server_client = server_cx
+ .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "mock-server", false));
+
+ let connection = Arc::new(MockRemoteConnection {
+ options: opts.clone(),
+ server_channel: server_client.clone(),
+ server_cx: SendableCx::new(server_cx),
+ });
+
+ client_cx.update(|cx| {
+ cx.default_global::<MockConnectionRegistry>()
+ .pending
+ .insert(opts.clone(), connection);
+ });
+
+ (opts, server_client.into())
+ }
+}
+
+#[async_trait(?Send)]
+impl RemoteConnection for MockRemoteConnection {
+ async fn kill(&self) -> Result<()> {
+ Ok(())
+ }
+
+ fn has_been_killed(&self) -> bool {
+ false
+ }
+
+ fn build_command(
+ &self,
+ program: Option<String>,
+ args: &[String],
+ env: &HashMap<String, String>,
+ _working_dir: Option<String>,
+ _port_forward: Option<(u16, String, u16)>,
+ ) -> Result<CommandTemplate> {
+ let shell_program = program.unwrap_or_else(|| "sh".to_string());
+ let mut shell_args = Vec::new();
+ shell_args.push(shell_program);
+ shell_args.extend(args.iter().cloned());
+ Ok(CommandTemplate {
+ program: "mock".into(),
+ args: shell_args,
+ env: env.clone(),
+ })
+ }
+
+ fn build_forward_ports_command(
+ &self,
+ forwards: Vec<(u16, String, u16)>,
+ ) -> Result<CommandTemplate> {
+ Ok(CommandTemplate {
+ program: "mock".into(),
+ args: std::iter::once("-N".to_owned())
+ .chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
+ format!("{local_port}:{host}:{remote_port}")
+ }))
+ .collect(),
+ env: Default::default(),
+ })
+ }
+
+ fn upload_directory(
+ &self,
+ _src_path: PathBuf,
+ _dest_path: RemotePathBuf,
+ _cx: &App,
+ ) -> Task<Result<()>> {
+ Task::ready(Ok(()))
+ }
+
+ fn connection_options(&self) -> RemoteConnectionOptions {
+ RemoteConnectionOptions::Mock(self.options.clone())
+ }
+
+ fn simulate_disconnect(&self, cx: &AsyncApp) {
+ let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
+ let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
+ self.server_channel
+ .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));
+ }
+
+ fn start_proxy(
+ &self,
+ _unique_identifier: String,
+ _reconnect: bool,
+ mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
+ mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
+ mut connection_activity_tx: Sender<()>,
+ _delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<i32>> {
+ let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
+ let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
+
+ self.server_channel.reconnect(
+ server_incoming_rx,
+ server_outgoing_tx,
+ &self.server_cx.get(cx),
+ );
+
+ cx.background_spawn(async move {
+ loop {
+ select_biased! {
+ server_to_client = server_outgoing_rx.next().fuse() => {
+ let Some(server_to_client) = server_to_client else {
+ return Ok(1)
+ };
+ connection_activity_tx.try_send(()).ok();
+ client_incoming_tx.send(server_to_client).await.ok();
+ }
+ client_to_server = client_outgoing_rx.next().fuse() => {
+ let Some(client_to_server) = client_to_server else {
+ return Ok(1)
+ };
+ server_incoming_tx.send(client_to_server).await.ok();
+ }
+ }
+ }
+ })
+ }
+
+ fn path_style(&self) -> PathStyle {
+ PathStyle::local()
+ }
+
+ fn shell(&self) -> String {
+ "sh".to_owned()
+ }
+
+ fn default_system_shell(&self) -> String {
+ "sh".to_owned()
+ }
+
+ fn has_wsl_interop(&self) -> bool {
+ false
+ }
+}
+
+/// Mock delegate for tests that don't need delegate functionality.
+pub struct MockDelegate;
+
+impl RemoteClientDelegate for MockDelegate {
+ fn ask_password(
+ &self,
+ _prompt: String,
+ _sender: futures::channel::oneshot::Sender<askpass::EncryptedPassword>,
+ _cx: &mut AsyncApp,
+ ) {
+ unreachable!("MockDelegate::ask_password should not be called in tests")
+ }
+
+ fn download_server_binary_locally(
+ &self,
+ _platform: crate::RemotePlatform,
+ _release_channel: release_channel::ReleaseChannel,
+ _version: Option<semver::Version>,
+ _cx: &mut AsyncApp,
+ ) -> Task<Result<PathBuf>> {
+ unreachable!("MockDelegate::download_server_binary_locally should not be called in tests")
+ }
+
+ fn get_download_url(
+ &self,
+ _platform: crate::RemotePlatform,
+ _release_channel: release_channel::ReleaseChannel,
+ _version: Option<semver::Version>,
+ _cx: &mut AsyncApp,
+ ) -> Task<Result<Option<String>>> {
+ unreachable!("MockDelegate::get_download_url should not be called in tests")
+ }
+
+ fn set_status(&self, _status: Option<&str>, _cx: &mut AsyncApp) {}
+}
@@ -1894,7 +1894,7 @@ async fn test_remote_external_agent_server(
assert_eq!(
command,
AgentServerCommand {
- path: "ssh".into(),
+ path: "mock".into(),
args: vec!["foo-cli".into(), "--flag".into()],
env: Some(HashMap::from_iter([
("VAR".into(), "val".into()),
@@ -22,8 +22,10 @@ test-support = [
"gpui/test-support",
"http_client/test-support",
"project/test-support",
+ "remote/test-support",
"util/test-support",
"workspace/test-support",
+ "recent_projects/test-support",
]
[dependencies]
@@ -366,6 +366,8 @@ impl TitleBar {
RemoteConnectionOptions::Docker(_dev_container_connection) => {
(None, "Dev Container", IconName::Box)
}
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(_) => (None, "Mock Remote Project", IconName::Server),
};
let nickname = nickname.unwrap_or_else(|| host.clone());
@@ -1325,6 +1325,11 @@ impl WorkspaceDb {
container_id = Some(options.container_id);
name = Some(options.name);
}
+ #[cfg(any(test, feature = "test-support"))]
+ RemoteConnectionOptions::Mock(options) => {
+ kind = RemoteConnectionKind::Ssh;
+ host = Some(format!("mock-{}", options.id));
+ }
}
Self::get_or_create_remote_connection_query(
this,
@@ -23,6 +23,7 @@ test-support = [
"editor/test-support",
"terminal_view/test-support",
"image_viewer/test-support",
+ "recent_projects/test-support",
]
visual-tests = [
"gpui/test-support",