mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
180 lines
7.6 KiB
Rust
180 lines
7.6 KiB
Rust
//! Deployment and run targets against the in-process SSH / SFTP server.
|
|
|
|
use ide_core::remote_settings::{DeploymentMapping, DeploymentServer, DeploymentSettings, RunTarget, SshAuth};
|
|
use ide_core::{CancellationToken, IdeEvent, SharedSink};
|
|
use ide_remote::{Deployment, Excludes, Mappings, RemoteRuns, RemoteStart, SyncAction, SyncStatus, SyncStep};
|
|
use ide_run::{OutputChunk, RunConfig};
|
|
use ide_ssh::{RemoteFs, Session, SshFiles, SshTarget};
|
|
use parking_lot::Mutex;
|
|
use std::sync::Arc;
|
|
|
|
struct Env {
|
|
_tmp: tempfile::TempDir,
|
|
project: std::path::PathBuf,
|
|
server_root: std::path::PathBuf,
|
|
session: Arc<Session>,
|
|
}
|
|
|
|
async fn env() -> Env {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let project = tmp.path().join("project");
|
|
let server_root = tmp.path().join("server");
|
|
std::fs::create_dir_all(project.join("src")).unwrap();
|
|
std::fs::create_dir_all(project.join(".git")).unwrap();
|
|
std::fs::create_dir_all(&server_root).unwrap();
|
|
std::fs::write(project.join("src/main.py"), "print('hi')\n").unwrap();
|
|
std::fs::write(project.join("README.md"), "# demo\n").unwrap();
|
|
std::fs::write(project.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
|
|
let port = ide_ssh::testing::sshd(server_root.clone(), Arc::new(|cmd| (format!("ran: {cmd}"), 0))).await;
|
|
let files = SshFiles { known_hosts: tmp.path().join("known_hosts"), config: None };
|
|
let target = SshTarget {
|
|
host: "127.0.0.1".into(),
|
|
port: Some(port),
|
|
user: "dev".into(),
|
|
auth: SshAuth::Password,
|
|
key_file: String::new(),
|
|
};
|
|
let session = match Session::connect(&target, Some("secret"), &files).await {
|
|
Err(ide_ssh::ConnectError::UnknownHostKey(k)) => {
|
|
k.trust(&files.known_hosts).unwrap();
|
|
Session::connect(&target, Some("secret"), &files).await.ok().unwrap()
|
|
}
|
|
other => other.ok().unwrap(),
|
|
};
|
|
Env { _tmp: tmp, project, server_root, session: Arc::new(session) }
|
|
}
|
|
|
|
fn server() -> DeploymentServer {
|
|
DeploymentServer {
|
|
id: "s".into(),
|
|
name: "staging".into(),
|
|
root_path: "/srv".into(),
|
|
mappings: vec![DeploymentMapping { local: String::new(), deployment: "app".into(), web: String::new() }],
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn no_progress() -> impl FnMut(&str, usize, usize) + Send {
|
|
|_, _, _| {}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn uploads_compares_syncs_and_downloads() {
|
|
let env = env().await;
|
|
let fs = RemoteFs::new(env.session.sftp().await.unwrap());
|
|
let server = server();
|
|
let settings = DeploymentSettings::default();
|
|
let map = Mappings::new(&server, &env.project, "/srv");
|
|
let exclude = Excludes::new(&settings, &server, "/srv");
|
|
let d = Deployment { fs: &fs, map: &map, exclude: &exclude, preserve_time: true, cancel: CancellationToken::new() };
|
|
|
|
let report = d.upload(std::slice::from_ref(&env.project), &mut no_progress()).await;
|
|
assert_eq!((report.files, report.errors.len()), (2, 0), "{report:?}");
|
|
assert_eq!(std::fs::read_to_string(env.server_root.join("srv/app/src/main.py")).unwrap(), "print('hi')\n");
|
|
assert!(!env.server_root.join("srv/app/.git").exists(), ".git is excluded by name");
|
|
|
|
let same = d.compare(&env.project).await.unwrap();
|
|
assert!(same.iter().all(|e| e.status == SyncStatus::Same), "{same:?}");
|
|
|
|
// A new local file, a file only on the server, a remote change.
|
|
std::fs::write(env.project.join("src/new.py"), "x = 1\n").unwrap();
|
|
std::fs::write(env.server_root.join("srv/app/remote.txt"), "server side\n").unwrap();
|
|
std::fs::write(env.server_root.join("srv/app/README.md"), "# changed on the server, longer\n").unwrap();
|
|
let file = std::fs::File::options().write(true).open(env.server_root.join("srv/app/README.md")).unwrap();
|
|
file.set_modified(std::time::SystemTime::now() + std::time::Duration::from_secs(60)).unwrap();
|
|
let entries = d.compare(&env.project).await.unwrap();
|
|
let status = |p: &str| entries.iter().find(|e| e.path == p).map(|e| e.status);
|
|
assert_eq!(status("src/new.py"), Some(SyncStatus::LocalOnly));
|
|
assert_eq!(status("remote.txt"), Some(SyncStatus::RemoteOnly));
|
|
assert_eq!(status("README.md"), Some(SyncStatus::RemoteNewer));
|
|
assert_eq!(status("src/main.py"), Some(SyncStatus::Same));
|
|
|
|
let steps = vec![
|
|
SyncStep { path: "src/new.py".into(), action: SyncAction::Upload },
|
|
SyncStep { path: "README.md".into(), action: SyncAction::Download },
|
|
SyncStep { path: "remote.txt".into(), action: SyncAction::DeleteRemote },
|
|
];
|
|
let report = d.apply(&steps, &mut no_progress()).await;
|
|
assert_eq!((report.files, report.errors.len()), (3, 0), "{report:?}");
|
|
assert!(env.server_root.join("srv/app/src/new.py").is_file());
|
|
assert!(!env.server_root.join("srv/app/remote.txt").exists());
|
|
assert_eq!(std::fs::read_to_string(env.project.join("README.md")).unwrap(), "# changed on the server, longer\n");
|
|
|
|
// Download a folder, then delete it remotely.
|
|
std::fs::remove_dir_all(env.project.join("src")).unwrap();
|
|
let report = d.download(&[env.project.join("src")], &mut no_progress()).await;
|
|
assert_eq!((report.files, report.errors.len()), (2, 0), "{report:?}");
|
|
assert!(env.project.join("src/main.py").is_file());
|
|
let report = d.delete_remote(&[env.project.join("src")]).await;
|
|
assert_eq!(report.files, 1);
|
|
assert!(!env.server_root.join("srv/app/src").exists());
|
|
|
|
// Sync before a run uploads only what changed.
|
|
let report = d.push_changes(&env.project, &mut no_progress()).await.unwrap();
|
|
assert_eq!(report.files, 2, "{report:?}");
|
|
assert_eq!(d.push_changes(&env.project, &mut no_progress()).await.unwrap().files, 0);
|
|
}
|
|
|
|
struct Sink(Mutex<Vec<IdeEvent>>);
|
|
|
|
impl ide_core::EventSink for Sink {
|
|
fn emit(&self, event: IdeEvent) {
|
|
self.0.lock().push(event);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runs_a_configuration_on_a_target() {
|
|
let env = env().await;
|
|
let runs = RemoteRuns::new();
|
|
let target =
|
|
RunTarget { id: "t".into(), name: "box".into(), python: "/usr/bin/python3".into(), ..Default::default() };
|
|
let config = RunConfig {
|
|
id: "main".into(),
|
|
name: "main".into(),
|
|
kind: "python".into(),
|
|
command: "python3".into(),
|
|
args: vec![env.project.join("src/main.py").to_string_lossy().into_owned()],
|
|
cwd: None,
|
|
env: Default::default(),
|
|
detected: false,
|
|
is_test: false,
|
|
debug_adapter: None,
|
|
allow_parallel: false,
|
|
extras: Default::default(),
|
|
};
|
|
let output: Arc<Mutex<String>> = Arc::default();
|
|
let out = output.clone();
|
|
let sink = Arc::new(Sink(Mutex::default()));
|
|
let start = RemoteStart {
|
|
session: env.session.clone(),
|
|
target,
|
|
root: env.project.clone(),
|
|
remote_root: "/home/dev/demo".into(),
|
|
sync: None,
|
|
};
|
|
let info = runs.start(
|
|
start,
|
|
&config,
|
|
sink.clone() as SharedSink,
|
|
Box::new(move |chunk| match chunk {
|
|
OutputChunk::Stdout { text } | OutputChunk::Stderr { text } | OutputChunk::System { text } => {
|
|
out.lock().push_str(&text)
|
|
}
|
|
}),
|
|
);
|
|
assert!(runs.contains(info.id));
|
|
for _ in 0..200 {
|
|
if !sink.0.lock().is_empty() {
|
|
break;
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
}
|
|
let events = sink.0.lock().clone();
|
|
assert!(matches!(events.as_slice(), [IdeEvent::RunExited { code: Some(0), .. }]), "{events:?}");
|
|
let text = output.lock().clone();
|
|
assert!(text.contains("ran: cd /home/dev/demo && exec /usr/bin/python3 /home/dev/demo/src/main.py"), "{text}");
|
|
assert!(text.contains("Process finished with exit code 0"), "{text}");
|
|
assert!(!runs.list()[0].running);
|
|
}
|