- 仍是一个测试程序(tests/ui/main.rs,cargo test --test ui),每个文件单独成测试程序会再链接一遍 GPUI - 按功能分 11 个模块:host_dialog、host_tree、connection、terminal、workspace、settings、 sftp、transfer、forward、credential、update - 两个以上模块共用的夹具和假实现放进 support/:mod.rs 是公共的 use、种子 id 和通用助手, 其余按终端、SFTP、端口转发、测试连接、凭据分文件;只有一个模块用的留在那个模块里 - 纯搬移,测试逻辑不变:147 个测试名称与搬移前一致,全部通过 - CLAUDE.md 写明模块划分和新测试该放哪里,AGENTS.md、README 改路径
318 lines
12 KiB
Rust
318 lines
12 KiB
Rust
//! Fake SFTP transports and local folders, and an SFTP tab opened on them.
|
|
|
|
use super::*;
|
|
|
|
#[derive(Default)]
|
|
pub struct FakeSftpProvider {
|
|
pub requests: Arc<Mutex<Vec<UploadRequest>>>,
|
|
pub downloads: Arc<Mutex<Vec<shellrs::sftp::DownloadRequest>>>,
|
|
pub operations: Arc<Mutex<Vec<shellrs::sftp::RemoteOperation>>>,
|
|
pub events: Arc<Mutex<Vec<async_channel::Sender<SftpEvent>>>>,
|
|
/// When set, the next connection waits for a message before it is up.
|
|
pub hold_connection: Arc<Mutex<Option<mpsc::Receiver<()>>>>,
|
|
/// Connections made again on 重新连接 (a bare 继续).
|
|
pub reconnects: Arc<Mutex<usize>>,
|
|
/// The remote home is `/slow`, which never answers: a network too slow
|
|
/// for the first directory to arrive.
|
|
pub slow_home: bool,
|
|
}
|
|
|
|
impl SftpTransportProvider for FakeSftpProvider {
|
|
fn create(&self, _: &HostLogin) -> Box<dyn SftpTransport> {
|
|
Box::new(FakeSftpTransport {
|
|
requests: self.requests.clone(),
|
|
downloads: self.downloads.clone(),
|
|
operations: self.operations.clone(),
|
|
events: self.events.clone(),
|
|
hold: self.hold_connection.lock().unwrap().take(),
|
|
reconnects: self.reconnects.clone(),
|
|
home: if self.slow_home {
|
|
"/slow"
|
|
} else {
|
|
"/home/tester"
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct FakeSftpTransport {
|
|
pub requests: Arc<Mutex<Vec<UploadRequest>>>,
|
|
pub downloads: Arc<Mutex<Vec<shellrs::sftp::DownloadRequest>>>,
|
|
pub operations: Arc<Mutex<Vec<shellrs::sftp::RemoteOperation>>>,
|
|
pub events: Arc<Mutex<Vec<async_channel::Sender<SftpEvent>>>>,
|
|
pub hold: Option<mpsc::Receiver<()>>,
|
|
pub reconnects: Arc<Mutex<usize>>,
|
|
pub home: &'static str,
|
|
}
|
|
|
|
impl SftpTransport for FakeSftpTransport {
|
|
fn run(
|
|
self: Box<Self>,
|
|
commands: async_channel::Receiver<SftpCommand>,
|
|
events: async_channel::Sender<SftpEvent>,
|
|
) -> anyhow::Result<()> {
|
|
use shellrs::sftp::{TransferChoice, TransferPhase, TransferProgress};
|
|
self.events.lock().unwrap().push(events.clone());
|
|
if let Some(hold) = &self.hold {
|
|
let _ = hold.recv();
|
|
}
|
|
events.send_blocking(SftpEvent::Connected {
|
|
home: RemotePath::new(self.home)?,
|
|
})?;
|
|
// As in the real engine, 继续 goes on with a stopped batch, and with
|
|
// none it only reconnects.
|
|
let mut stopped = false;
|
|
while let Ok(command) = commands.recv_blocking() {
|
|
match command {
|
|
SftpCommand::List { request_id, path } => {
|
|
if path.as_str() == "/slow" {
|
|
continue;
|
|
}
|
|
let result = match path.as_str() {
|
|
"/denied" => Err("权限不足".into()),
|
|
"/empty" => Ok(DirectoryListing::new("/empty", vec![])),
|
|
_ => Ok(fake_listing(path.as_str())),
|
|
};
|
|
events.send_blocking(SftpEvent::Listed { request_id, result })?;
|
|
}
|
|
SftpCommand::Operate {
|
|
request_id,
|
|
operation,
|
|
} => {
|
|
let refused = format!("{operation:?}").contains("denied");
|
|
self.operations.lock().unwrap().push(operation);
|
|
events.send_blocking(SftpEvent::Operated {
|
|
request_id,
|
|
result: if refused {
|
|
Err("权限不足".into())
|
|
} else {
|
|
Ok(())
|
|
},
|
|
})?;
|
|
}
|
|
SftpCommand::Download(request) => {
|
|
self.downloads.lock().unwrap().push(request);
|
|
events.send_blocking(SftpEvent::Progress(
|
|
TransferProgress::new(TransferPhase::Transferring)
|
|
.with_direction(shellrs::sftp::TransferDirection::Download),
|
|
))?;
|
|
}
|
|
SftpCommand::Upload(request) => {
|
|
self.requests.lock().unwrap().push(request);
|
|
events.send_blocking(SftpEvent::Progress(Default::default()))?;
|
|
}
|
|
// The real engine says Idle once a batch stops or ends,
|
|
// which is when the queue may move on.
|
|
SftpCommand::Cancel => {
|
|
stopped = true;
|
|
events.send_blocking(SftpEvent::Progress(TransferProgress::new(
|
|
TransferPhase::Stopped,
|
|
)))?;
|
|
events.send_blocking(SftpEvent::Idle)?;
|
|
}
|
|
SftpCommand::Discard => {
|
|
stopped = false;
|
|
events.send_blocking(SftpEvent::Progress(TransferProgress::new(
|
|
TransferPhase::Completed,
|
|
)))?;
|
|
events.send_blocking(SftpEvent::Idle)?;
|
|
}
|
|
SftpCommand::Resume if stopped => {
|
|
stopped = false;
|
|
events.send_blocking(SftpEvent::Progress(TransferProgress::new(
|
|
TransferPhase::Transferring,
|
|
)))?;
|
|
}
|
|
SftpCommand::Resume => {
|
|
*self.reconnects.lock().unwrap() += 1;
|
|
events.send_blocking(SftpEvent::Connected {
|
|
home: RemotePath::new("/home/tester")?,
|
|
})?;
|
|
events.send_blocking(SftpEvent::Idle)?;
|
|
}
|
|
SftpCommand::Answer { answer, .. } => {
|
|
stopped = answer.choice() == TransferChoice::Cancel;
|
|
events.send_blocking(SftpEvent::Progress(TransferProgress::new(
|
|
if answer.choice() == TransferChoice::Cancel {
|
|
TransferPhase::Stopped
|
|
} else {
|
|
TransferPhase::Completed
|
|
},
|
|
)))?;
|
|
events.send_blocking(SftpEvent::Idle)?;
|
|
}
|
|
SftpCommand::Shutdown => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// The local file system of the tests: every directory lists
|
|
/// `fake_listing`, and changes are recorded, never made.
|
|
#[derive(Clone, Default)]
|
|
pub struct FakeLocalDirectory {
|
|
pub calls: Arc<Mutex<Vec<String>>>,
|
|
}
|
|
|
|
impl FakeLocalDirectory {
|
|
pub fn record(&self, call: String) -> anyhow::Result<()> {
|
|
self.calls.lock().unwrap().push(call);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl LocalDirectoryProvider for FakeLocalDirectory {
|
|
fn home(&self) -> std::path::PathBuf {
|
|
"/local/tester".into()
|
|
}
|
|
fn list(&self, path: &std::path::Path) -> anyhow::Result<DirectoryListing> {
|
|
Ok(fake_listing(path.to_str().unwrap()))
|
|
}
|
|
fn trash(&self, paths: &[std::path::PathBuf]) -> anyhow::Result<()> {
|
|
self.record(format!("trash {paths:?}"))
|
|
}
|
|
fn rename(&self, from: &std::path::Path, to: &std::path::Path) -> anyhow::Result<()> {
|
|
self.record(format!("rename {} -> {}", from.display(), to.display()))
|
|
}
|
|
fn create_dir(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
|
self.record(format!("mkdir {}", path.display()))
|
|
}
|
|
fn create_file(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
|
self.record(format!("touch {}", path.display()))
|
|
}
|
|
fn set_permissions(
|
|
&self,
|
|
paths: &[std::path::PathBuf],
|
|
edit: shellrs::sftp::PermissionEdit,
|
|
recursive: bool,
|
|
add_x_to_dirs: bool,
|
|
) -> anyhow::Result<()> {
|
|
self.record(format!(
|
|
"chmod {paths:?} +{:o} -{:o} recursive={recursive} x={add_x_to_dirs}",
|
|
edit.set(),
|
|
edit.clear()
|
|
))
|
|
}
|
|
}
|
|
|
|
pub fn fake_listing(path: &str) -> DirectoryListing {
|
|
DirectoryListing::new(
|
|
path,
|
|
vec![
|
|
DirectoryEntry::new(
|
|
"文件 甲.txt",
|
|
FileMetadata::new(EntryKind::File, 12, Some(100), Some(0o644)),
|
|
),
|
|
DirectoryEntry::new(
|
|
"文件 乙.txt",
|
|
FileMetadata::new(EntryKind::File, 34, Some(200), Some(0o644)),
|
|
),
|
|
DirectoryEntry::new(
|
|
"目录",
|
|
FileMetadata::new(EntryKind::Directory, 0, None, Some(0o755)),
|
|
),
|
|
DirectoryEntry::new(
|
|
"链接目录",
|
|
FileMetadata::new(EntryKind::Symlink, 7, Some(300), Some(0o120_777)),
|
|
)
|
|
.with_target_kind(Some(EntryKind::Directory)),
|
|
]
|
|
.into_iter()
|
|
.map(|entry| entry.with_owner(Some("root".into()), Some("wheel".into())))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
pub fn open_workspace_with_sftp(
|
|
cx: &mut TestAppContext,
|
|
provider: Arc<FakeSftpProvider>,
|
|
) -> (WindowHandle<Root>, Entity<Workspace>) {
|
|
open_workspace_with_services(cx, provider, FakeLocalDirectory::default())
|
|
}
|
|
|
|
pub fn open_workspace_with_services(
|
|
cx: &mut TestAppContext,
|
|
provider: Arc<FakeSftpProvider>,
|
|
local: FakeLocalDirectory,
|
|
) -> (WindowHandle<Root>, Entity<Workspace>) {
|
|
cx.update(shellrs::init);
|
|
// Dialogs slide in over real time; small targets such as checkboxes
|
|
// would move between the frame that locates them and the click.
|
|
cx.update(|cx| cx.set_reduce_motion(true));
|
|
let mut workspace = None;
|
|
let handle = cx.open_window(size(px(1280.), px(800.)), |window, cx| {
|
|
let store = cx.new(|_| HostStore::seed());
|
|
let remote = Arc::new(FixedRemoteTerminalTransportProvider::new(Arc::new(
|
|
FakeTerminalFactory::default(),
|
|
)));
|
|
let view = cx.new(|cx| {
|
|
Workspace::new_with_services(
|
|
store,
|
|
cx.new(|_| SettingsStore::in_memory()),
|
|
remote,
|
|
Arc::new(FakeTerminalFactory::default()),
|
|
provider,
|
|
Arc::new(local),
|
|
Arc::new(FakeConnectionTester::default()),
|
|
Arc::new(FakeForwardProvider::default()),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
workspace = Some(view.clone());
|
|
Root::new(view, window, cx)
|
|
});
|
|
(handle, workspace.unwrap())
|
|
}
|
|
|
|
pub async fn open_test_explorer(cx: &mut TestAppContext, handle: WindowHandle<Root>) {
|
|
cx.update_window(handle.into(), |_, window, cx| {
|
|
window.render_frame(cx);
|
|
window.dispatch_action(Box::new(OpenExplorer(HostId(DB_01))), cx);
|
|
})
|
|
.unwrap();
|
|
cx.wait_for(handle.into(), Duration::from_secs(2), |window, cx| {
|
|
window.render_frame(cx);
|
|
window
|
|
.try_find("remote-path")
|
|
.is_some_and(|p| p.value() == Some("/home/tester"))
|
|
&& window
|
|
.within(("local-pane", SFTP_TAB))
|
|
.try_find("file:文件 甲.txt")
|
|
.is_some()
|
|
})
|
|
.await;
|
|
}
|
|
|
|
/// The selected names of one explorer pane, in display order.
|
|
pub fn pane_selection(workspace: &Entity<Workspace>, remote: bool, cx: &App) -> Vec<String> {
|
|
workspace
|
|
.read(cx)
|
|
.explorer(ExplorerId(SFTP_TAB))
|
|
.unwrap()
|
|
.read(cx)
|
|
.pane(remote)
|
|
.read(cx)
|
|
.selected_names(cx)
|
|
}
|
|
|
|
/// Click a row in one pane, then press a key with the list focused.
|
|
pub fn press_on_row(
|
|
cx: &mut TestAppContext,
|
|
handle: WindowHandle<Root>,
|
|
pane: &'static str,
|
|
row: &str,
|
|
key: &str,
|
|
) {
|
|
let row = ElementId::Name(format!("name:{row}").into());
|
|
cx.update_window(handle.into(), |_, window, cx| {
|
|
window.render_frame(cx);
|
|
window.within((pane, SFTP_TAB)).click(row, cx);
|
|
window.press(key, cx);
|
|
})
|
|
.unwrap();
|
|
cx.run_until_parked();
|
|
}
|