//! Fake SFTP transports and local folders, and an SFTP tab opened on them. use super::*; #[derive(Default)] pub struct FakeSftpProvider { pub requests: Arc>>, pub downloads: Arc>>, pub operations: Arc>>, pub events: Arc>>>, /// When set, the next connection waits for a message before it is up. pub hold_connection: Arc>>>, /// Connections made again on 重新连接 (a bare 继续). pub reconnects: Arc>, /// 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 { 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>>, pub downloads: Arc>>, pub operations: Arc>>, pub events: Arc>>>, pub hold: Option>, pub reconnects: Arc>, pub home: &'static str, } impl SftpTransport for FakeSftpTransport { fn run( self: Box, commands: async_channel::Receiver, events: async_channel::Sender, ) -> 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>>, } 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 { 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, ) -> (WindowHandle, Entity) { open_workspace_with_services(cx, provider, FakeLocalDirectory::default()) } pub fn open_workspace_with_services( cx: &mut TestAppContext, provider: Arc, local: FakeLocalDirectory, ) -> (WindowHandle, Entity) { 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) { 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, remote: bool, cx: &App) -> Vec { 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, 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(); }