Files
shell-rs/tests/ui/support/sftp.rs
Xuz 388a1aa68a UI 测试按功能拆成模块:tests/ui.rs → tests/ui/
- 仍是一个测试程序(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 改路径
2026-10-01 14:53:33 +08:00

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();
}