use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use tempfile::TempDir;
const FEDORA_HOST: &str = "fedora";
const FEDORA_USER: &str = "nick";
fn create_fedora_config() -> msy::ssh::config::SshConfig {
use msy::ssh::config::SshConfig;
let mut config = SshConfig::new(FEDORA_HOST);
config.user = FEDORA_USER.to_string();
config.port = 22;
config
}
fn create_remote_test_path(test_name: &str) -> String {
format!("/tmp/sy_bisync_test_{}_{}", test_name, std::process::id())
}
fn cleanup_remote_path(path: &str) {
let cleanup_cmd = format!("ssh {} 'rm -rf {}'", FEDORA_HOST, path);
let _ = std::process::Command::new("sh").arg("-c").arg(&cleanup_cmd).output();
}
fn create_local_file(path: &Path, content: &[u8]) {
fs::write(path, content).expect("Failed to create local file");
}
fn read_remote_file(remote_path: &str) -> Vec<u8> {
let read_cmd = format!("ssh {} 'cat {}'", FEDORA_HOST, remote_path);
let output = std::process::Command::new("sh").arg("-c").arg(&read_cmd).output().expect("Failed to read remote file");
if !output.status.success() {
panic!("Failed to read remote file: {}", String::from_utf8_lossy(&output.stderr));
}
output.stdout
}
fn create_remote_file(remote_path: &str, content: &[u8]) {
use std::io::Write;
use std::process::{Command, Stdio};
let mut child = Command::new("sh")
.arg("-c")
.arg(format!("ssh {} 'cat > {}'", FEDORA_HOST, remote_path))
.stdin(Stdio::piped())
.spawn()
.expect("Failed to spawn ssh");
child.stdin.as_mut().unwrap().write_all(content).unwrap();
let status = child.wait().expect("Failed to wait for ssh");
if !status.success() {
panic!("Failed to create remote file");
}
}
#[tokio::test]
#[ignore]
async fn test_bisync_initial_local_to_remote() {
use msy::bisync::{BisyncEngine, BisyncOptions};
use msy::transport::local::LocalTransport;
use msy::transport::ssh::SshTransport;
let local_dir = TempDir::new().unwrap();
let remote_dir = create_remote_test_path("initial_l2r");
create_local_file(&local_dir.path().join("file1.txt"), b"content 1");
create_local_file(&local_dir.path().join("file2.txt"), b"content 2");
let create_cmd = format!("ssh {} 'mkdir -p {}'", FEDORA_HOST, remote_dir);
std::process::Command::new("sh").arg("-c").arg(&create_cmd).output().expect("Failed to create remote directory");
let local_transport = Arc::new(LocalTransport::new());
let remote_config = create_fedora_config();
let remote_transport = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine = BisyncEngine::new(local_transport, remote_transport);
let result = engine.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Initial sync failed");
assert!(result.stats.files_synced_to_dest >= 2, "Should copy at least 2 files in initial sync");
let file1_content = read_remote_file(&format!("{}/file1.txt", remote_dir));
let file2_content = read_remote_file(&format!("{}/file2.txt", remote_dir));
assert_eq!(file1_content, b"content 1");
assert_eq!(file2_content, b"content 2");
cleanup_remote_path(&remote_dir);
println!("✅ bisync_initial_local_to_remote: PASS");
}
#[tokio::test]
#[ignore]
async fn test_bisync_initial_remote_to_local() {
use msy::bisync::{BisyncEngine, BisyncOptions};
use msy::transport::local::LocalTransport;
use msy::transport::ssh::SshTransport;
let local_dir = TempDir::new().unwrap();
let remote_dir = create_remote_test_path("initial_r2l");
let create_cmd = format!(
"ssh {} 'mkdir -p {} && echo \"remote 1\" > {}/file1.txt && echo \"remote 2\" > {}/file2.txt'",
FEDORA_HOST, remote_dir, remote_dir, remote_dir
);
std::process::Command::new("sh").arg("-c").arg(&create_cmd).output().expect("Failed to create remote files");
let local_transport = Arc::new(LocalTransport::new());
let remote_config = create_fedora_config();
let remote_transport = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine = BisyncEngine::new(local_transport, remote_transport);
let result = engine.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Initial sync failed");
assert!(result.stats.files_synced_to_source >= 2, "Should copy at least 2 files from remote");
let file1 = fs::read(local_dir.path().join("file1.txt")).unwrap();
let file2 = fs::read(local_dir.path().join("file2.txt")).unwrap();
assert_eq!(file1.trim_ascii(), b"remote 1");
assert_eq!(file2.trim_ascii(), b"remote 2");
cleanup_remote_path(&remote_dir);
println!("✅ bisync_initial_remote_to_local: PASS");
}
#[tokio::test]
#[ignore]
async fn test_bisync_add_file_to_local() {
use msy::bisync::{BisyncEngine, BisyncOptions};
use msy::transport::local::LocalTransport;
use msy::transport::ssh::SshTransport;
let local_dir = TempDir::new().unwrap();
let remote_dir = create_remote_test_path("add_local");
create_local_file(&local_dir.path().join("existing.txt"), b"exists");
let create_cmd = format!("ssh {} 'mkdir -p {}'", FEDORA_HOST, remote_dir);
std::process::Command::new("sh").arg("-c").arg(&create_cmd).output().expect("Failed to create remote directory");
let local_transport = Arc::new(LocalTransport::new());
let remote_config = create_fedora_config();
let remote_transport = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine = BisyncEngine::new(local_transport.clone(), remote_transport.clone());
engine.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Initial sync failed");
sleep(Duration::from_secs(1)); create_local_file(&local_dir.path().join("new.txt"), b"new content");
let local_transport2 = Arc::new(LocalTransport::new());
let remote_transport2 = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine2 = BisyncEngine::new(local_transport2, remote_transport2);
let result = engine2.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Second sync failed");
assert!(result.stats.files_synced_to_dest >= 1, "Should copy at least 1 new file");
let new_content = read_remote_file(&format!("{}/new.txt", remote_dir));
assert_eq!(new_content, b"new content");
cleanup_remote_path(&remote_dir);
println!("✅ bisync_add_file_to_local: PASS");
}
#[tokio::test]
#[ignore]
async fn test_bisync_add_file_to_remote() {
use msy::bisync::{BisyncEngine, BisyncOptions};
use msy::transport::local::LocalTransport;
use msy::transport::ssh::SshTransport;
let local_dir = TempDir::new().unwrap();
let remote_dir = create_remote_test_path("add_remote");
create_local_file(&local_dir.path().join("existing.txt"), b"exists");
let create_cmd = format!("ssh {} 'mkdir -p {}'", FEDORA_HOST, remote_dir);
std::process::Command::new("sh").arg("-c").arg(&create_cmd).output().expect("Failed to create remote directory");
let local_transport = Arc::new(LocalTransport::new());
let remote_config = create_fedora_config();
let remote_transport = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine = BisyncEngine::new(local_transport, remote_transport);
engine.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Initial sync failed");
sleep(Duration::from_secs(1));
create_remote_file(&format!("{}/remote_new.txt", remote_dir), b"remote new");
let local_transport2 = Arc::new(LocalTransport::new());
let remote_transport2 = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine2 = BisyncEngine::new(local_transport2, remote_transport2);
let result = engine2.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Second sync failed");
assert!(result.stats.files_synced_to_source >= 1, "Should copy at least 1 file from remote");
let local_new = fs::read(local_dir.path().join("remote_new.txt")).unwrap();
assert_eq!(local_new, b"remote new");
cleanup_remote_path(&remote_dir);
println!("✅ bisync_add_file_to_remote: PASS");
}
#[tokio::test]
#[ignore]
async fn test_bisync_delete_from_local() {
use msy::bisync::{BisyncEngine, BisyncOptions};
use msy::transport::local::LocalTransport;
use msy::transport::ssh::SshTransport;
let local_dir = TempDir::new().unwrap();
let remote_dir = create_remote_test_path("delete_local");
create_local_file(&local_dir.path().join("keep.txt"), b"keep");
create_local_file(&local_dir.path().join("delete.txt"), b"delete me");
let create_cmd = format!("ssh {} 'mkdir -p {}'", FEDORA_HOST, remote_dir);
std::process::Command::new("sh").arg("-c").arg(&create_cmd).output().expect("Failed to create remote directory");
let local_transport = Arc::new(LocalTransport::new());
let remote_config = create_fedora_config();
let remote_transport = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine = BisyncEngine::new(local_transport, remote_transport);
engine.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Initial sync failed");
fs::remove_file(local_dir.path().join("delete.txt")).unwrap();
let local_transport2 = Arc::new(LocalTransport::new());
let remote_transport2 = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine2 = BisyncEngine::new(local_transport2, remote_transport2);
let result = engine2
.sync(
local_dir.path(),
&PathBuf::from(&remote_dir),
BisyncOptions {
max_delete_percent: 0, ..BisyncOptions::default()
},
)
.await
.expect("Second sync failed");
assert!(result.stats.files_deleted_from_dest >= 1, "Should delete at least 1 file from remote");
let check_cmd = format!("ssh {} 'test -f {}/delete.txt && echo exists || echo missing'", FEDORA_HOST, remote_dir);
let output = std::process::Command::new("sh").arg("-c").arg(&check_cmd).output().expect("Failed to check remote file");
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "missing");
cleanup_remote_path(&remote_dir);
println!("✅ bisync_delete_from_local: PASS");
}
#[tokio::test]
#[ignore]
async fn test_bisync_conflict_newer_wins() {
use msy::bisync::{BisyncEngine, BisyncOptions, ConflictResolution};
use msy::transport::local::LocalTransport;
use msy::transport::ssh::SshTransport;
let local_dir = TempDir::new().unwrap();
let remote_dir = create_remote_test_path("conflict_newer");
create_local_file(&local_dir.path().join("conflict.txt"), b"original");
let create_cmd = format!("ssh {} 'mkdir -p {}'", FEDORA_HOST, remote_dir);
std::process::Command::new("sh").arg("-c").arg(&create_cmd).output().expect("Failed to create remote directory");
let local_transport = Arc::new(LocalTransport::new());
let remote_config = create_fedora_config();
let remote_transport = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine = BisyncEngine::new(local_transport, remote_transport);
engine.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Initial sync failed");
create_local_file(&local_dir.path().join("conflict.txt"), b"local version");
sleep(Duration::from_secs(2));
create_remote_file(&format!("{}/conflict.txt", remote_dir), b"remote version newer");
let local_transport2 = Arc::new(LocalTransport::new());
let remote_transport2 = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine2 = BisyncEngine::new(local_transport2, remote_transport2);
let opts = BisyncOptions { conflict_resolution: ConflictResolution::Newer, ..Default::default() };
let result = engine2.sync(local_dir.path(), &PathBuf::from(&remote_dir), opts).await.expect("Sync with conflict failed");
assert!(
result.stats.conflicts_resolved >= 1 || result.stats.files_synced_to_source >= 1,
"Should resolve conflict or sync newer file"
);
let local_content = fs::read(local_dir.path().join("conflict.txt")).unwrap();
assert_eq!(local_content, b"remote version newer");
cleanup_remote_path(&remote_dir);
println!("✅ bisync_conflict_newer_wins: PASS");
}
#[tokio::test]
#[ignore]
async fn test_bisync_conflict_larger_wins() {
use msy::bisync::{BisyncEngine, BisyncOptions, ConflictResolution};
use msy::transport::local::LocalTransport;
use msy::transport::ssh::SshTransport;
let local_dir = TempDir::new().unwrap();
let remote_dir = create_remote_test_path("conflict_larger");
create_local_file(&local_dir.path().join("conflict.txt"), b"original");
let create_cmd = format!("ssh {} 'mkdir -p {}'", FEDORA_HOST, remote_dir);
std::process::Command::new("sh").arg("-c").arg(&create_cmd).output().expect("Failed to create remote directory");
let local_transport = Arc::new(LocalTransport::new());
let remote_config = create_fedora_config();
let remote_transport = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine = BisyncEngine::new(local_transport, remote_transport);
engine.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Initial sync failed");
sleep(Duration::from_secs(1));
create_local_file(&local_dir.path().join("conflict.txt"), b"small");
create_remote_file(&format!("{}/conflict.txt", remote_dir), b"this is much larger content");
let local_transport2 = Arc::new(LocalTransport::new());
let remote_transport2 = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine2 = BisyncEngine::new(local_transport2, remote_transport2);
let opts = BisyncOptions { conflict_resolution: ConflictResolution::Larger, ..Default::default() };
let result = engine2.sync(local_dir.path(), &PathBuf::from(&remote_dir), opts).await.expect("Sync failed");
assert!(
result.stats.conflicts_resolved >= 1 || result.stats.files_synced_to_source >= 1,
"Should resolve conflict or sync larger file"
);
let local_content = fs::read(local_dir.path().join("conflict.txt")).unwrap();
assert_eq!(local_content, b"this is much larger content");
cleanup_remote_path(&remote_dir);
println!("✅ bisync_conflict_larger_wins: PASS");
}
#[tokio::test]
#[ignore]
async fn test_bisync_max_delete_limit() {
use msy::bisync::{BisyncEngine, BisyncOptions};
use msy::transport::local::LocalTransport;
use msy::transport::ssh::SshTransport;
let local_dir = TempDir::new().unwrap();
let remote_dir = create_remote_test_path("max_delete");
for i in 0..10 {
create_local_file(&local_dir.path().join(format!("file{}.txt", i)), b"content");
}
let create_cmd = format!("ssh {} 'mkdir -p {}'", FEDORA_HOST, remote_dir);
std::process::Command::new("sh").arg("-c").arg(&create_cmd).output().expect("Failed to create remote directory");
let local_transport = Arc::new(LocalTransport::new());
let remote_config = create_fedora_config();
let remote_transport = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine = BisyncEngine::new(local_transport, remote_transport);
engine.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await.expect("Initial sync failed");
for i in 0..6 {
fs::remove_file(local_dir.path().join(format!("file{}.txt", i))).unwrap();
}
let local_transport2 = Arc::new(LocalTransport::new());
let remote_transport2 = Arc::new(SshTransport::new(&remote_config).await.expect("Failed to connect"));
let engine2 = BisyncEngine::new(local_transport2, remote_transport2);
let result = engine2.sync(local_dir.path(), &PathBuf::from(&remote_dir), BisyncOptions::default()).await;
assert!(result.is_err(), "Sync should fail when deletions exceed max_delete");
cleanup_remote_path(&remote_dir);
println!("✅ bisync_max_delete_limit: PASS");
}
#[test]
#[ignore]
fn print_bisync_test_summary() {
println!("\n========================================");
println!("SSH Bidirectional Sync Test Suite");
println!("========================================\n");
println!("SECTION 1: Initial Sync");
println!(" - initial_local_to_remote");
println!(" - initial_remote_to_local");
println!("\nSECTION 2: Incremental Sync");
println!(" - add_file_to_local");
println!(" - add_file_to_remote");
println!(" - delete_from_local");
println!("\nSECTION 3: Conflict Resolution");
println!(" - conflict_newer_wins");
println!(" - conflict_larger_wins");
println!("\nSECTION 4: Safety Features");
println!(" - max_delete_limit");
println!("\nTotal: 8 tests");
println!("\nRun with:");
println!(" cargo test --test ssh_bisync_test -- --ignored");
println!("========================================\n");
}