use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use std::process::Command;
use tempfile::TempDir;
fn sy_bin() -> String {
env!("CARGO_BIN_EXE_sy").to_string()
}
fn setup_test_dir(_name: &str) -> (TempDir, TempDir) {
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
Command::new("git").args(["init"]).current_dir(source.path()).output().unwrap();
(source, dest)
}
#[test]
fn test_basic_sync() {
let (source, dest) = setup_test_dir("basic");
fs::write(source.path().join("file1.txt"), "content1").unwrap();
fs::write(source.path().join("file2.txt"), "content2").unwrap();
let output = Command::new(sy_bin()).args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap()]).output().unwrap();
assert!(output.status.success());
assert!(dest.path().join("file1.txt").exists());
assert!(dest.path().join("file2.txt").exists());
assert_eq!(fs::read_to_string(dest.path().join("file1.txt")).unwrap(), "content1");
}
#[test]
fn test_dry_run() {
let (source, dest) = setup_test_dir("dry_run");
fs::write(source.path().join("file.txt"), "content").unwrap();
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--dry-run"])
.output()
.unwrap();
assert!(output.status.success());
assert!(!dest.path().join("file.txt").exists());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Dry-run"));
}
#[test]
fn test_delete_mode() {
let (source, dest) = setup_test_dir("delete");
fs::write(source.path().join("keep.txt"), "keep").unwrap();
fs::write(dest.path().join("keep.txt"), "keep").unwrap();
fs::write(dest.path().join("delete.txt"), "delete").unwrap();
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--delete"])
.output()
.unwrap();
assert!(output.status.success());
assert!(dest.path().join("keep.txt").exists());
assert!(!dest.path().join("delete.txt").exists());
}
#[test]
fn test_gitignore_support() {
let (source, dest) = setup_test_dir("gitignore");
fs::write(source.path().join(".gitignore"), "*.log\n").unwrap();
fs::write(source.path().join("keep.txt"), "keep").unwrap();
fs::write(source.path().join("ignore.log"), "ignore").unwrap();
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--gitignore"])
.output()
.unwrap();
assert!(output.status.success());
assert!(dest.path().join("keep.txt").exists());
assert!(!dest.path().join("ignore.log").exists());
assert!(dest.path().join(".gitignore").exists());
}
#[test]
fn test_nested_directories() {
let (source, dest) = setup_test_dir("nested");
fs::create_dir_all(source.path().join("dir1/dir2/dir3")).unwrap();
fs::write(source.path().join("dir1/file1.txt"), "content1").unwrap();
fs::write(source.path().join("dir1/dir2/file2.txt"), "content2").unwrap();
fs::write(source.path().join("dir1/dir2/dir3/file3.txt"), "content3").unwrap();
let output = Command::new(sy_bin()).args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap()]).output().unwrap();
assert!(output.status.success());
assert!(dest.path().join("dir1/file1.txt").exists());
assert!(dest.path().join("dir1/dir2/file2.txt").exists());
assert!(dest.path().join("dir1/dir2/dir3/file3.txt").exists());
}
#[test]
fn test_update_existing_files() {
let (source, dest) = setup_test_dir("update");
fs::write(source.path().join("file.txt"), "v1").unwrap();
Command::new(sy_bin()).args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap()]).output().unwrap();
assert_eq!(fs::read_to_string(dest.path().join("file.txt")).unwrap(), "v1");
std::thread::sleep(std::time::Duration::from_secs(2));
fs::write(source.path().join("file.txt"), "v2").unwrap();
let output = Command::new(sy_bin())
.args(["-v", &format!("{}/", source.path().display()), dest.path().to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
assert_eq!(fs::read_to_string(dest.path().join("file.txt")).unwrap(), "v2");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Files updated: 1"));
}
#[test]
fn test_skip_unchanged_files() {
let (source, dest) = setup_test_dir("skip");
fs::write(source.path().join("file.txt"), "content").unwrap();
Command::new(sy_bin())
.args(["-v", &format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--exclude-vcs"])
.output()
.unwrap();
let output = Command::new(sy_bin())
.args(["-v", &format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--exclude-vcs"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Files skipped: 1"));
}
#[test]
fn test_quiet_mode() {
let (source, dest) = setup_test_dir("quiet");
fs::write(source.path().join("file.txt"), "content").unwrap();
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--quiet"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!stdout.contains("sy v"));
}
#[test]
fn test_error_source_not_exists() {
let dest = TempDir::new().unwrap();
let output = Command::new(sy_bin()).args(["/nonexistent/path", dest.path().to_str().unwrap()]).output().unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("does not exist"));
}
#[tokio::test]
async fn test_single_file_sync() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("file.txt");
fs::write(&file_path, "test content for single file").unwrap();
let dest_file = temp.path().join("dest.txt");
let output = Command::new(sy_bin()).args([file_path.to_str().unwrap(), dest_file.to_str().unwrap()]).output().unwrap();
assert!(
output.status.success(),
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(dest_file.exists());
assert_eq!(fs::read_to_string(&dest_file).unwrap(), "test content for single file");
}
#[test]
fn test_git_directory_excluded() {
let (source, dest) = setup_test_dir("git_exclude");
fs::write(source.path().join(".git/config"), "test").unwrap();
fs::write(source.path().join("file.txt"), "content").unwrap();
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--exclude-vcs"])
.output()
.unwrap();
assert!(output.status.success());
assert!(dest.path().join("file.txt").exists());
assert!(!dest.path().join(".git").exists());
}
#[test]
fn test_update_shows_correct_stats() {
let (source, dest) = setup_test_dir("update_stats");
fs::write(source.path().join("file1.txt"), "initial content v1").unwrap();
fs::write(source.path().join("file2.txt"), "initial content v2").unwrap();
let output = Command::new(sy_bin())
.args(["-v", &format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--exclude-vcs"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Files created: 2"));
std::thread::sleep(std::time::Duration::from_secs(2));
fs::write(source.path().join("file1.txt"), "updated content v1").unwrap();
fs::write(source.path().join("file2.txt"), "updated content v2").unwrap();
let output = Command::new(sy_bin())
.args(["-v", &format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--exclude-vcs"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Files updated: 2"));
assert!(stdout.contains("Files skipped: 0"));
assert_eq!(fs::read_to_string(dest.path().join("file1.txt")).unwrap(), "updated content v1");
assert_eq!(fs::read_to_string(dest.path().join("file2.txt")).unwrap(), "updated content v2");
}
#[test]
#[ignore] fn test_large_file_update_with_delta_sync() {
let (source, dest) = setup_test_dir("delta_sync");
let large_file = source.path().join("large.bin");
let file = fs::File::create(&large_file).unwrap();
file.set_len(2 * 1024 * 1024 * 1024).unwrap(); drop(file);
let mut file = fs::OpenOptions::new().write(true).open(&large_file).unwrap();
file.write_all(b"START OF FILE").unwrap();
file.seek(SeekFrom::End(-13)).unwrap();
file.write_all(b"END OF FILE!!").unwrap();
drop(file);
let output = Command::new(sy_bin()).args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap()]).output().unwrap();
assert!(output.status.success());
assert!(dest.path().join("large.bin").exists());
std::thread::sleep(std::time::Duration::from_secs(2));
let mut file = fs::OpenOptions::new().write(true).open(&large_file).unwrap();
file.write_all(b"MODIFIED FILE").unwrap();
drop(file);
let output = Command::new(sy_bin())
.args(["-v", &format!("{}/", source.path().display()), dest.path().to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Files updated: 1"));
if stdout.contains("Delta sync:") {
assert!(stdout.contains("1 files"));
}
let dest_file = dest.path().join("large.bin");
let mut file = fs::File::open(&dest_file).unwrap();
let mut buf = [0u8; 13];
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"MODIFIED FILE");
}
#[test]
fn test_directory_cache_created() {
let (source, dest) = setup_test_dir("cache_created");
fs::create_dir_all(source.path().join("dir1/dir2")).unwrap();
fs::write(source.path().join("file1.txt"), "content1").unwrap();
fs::write(source.path().join("dir1/file2.txt"), "content2").unwrap();
fs::write(source.path().join("dir1/dir2/file3.txt"), "content3").unwrap();
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--use-cache=true"])
.output()
.unwrap();
assert!(output.status.success());
let cache_file = dest.path().join(".sy-dir-cache.json");
assert!(cache_file.exists(), "Directory cache file should be created at {}", cache_file.display());
let cache_content = fs::read_to_string(&cache_file).unwrap();
let cache_json: serde_json::Value = serde_json::from_str(&cache_content).unwrap();
assert!(cache_json.get("directories").is_some());
assert!(cache_json.get("version").is_some());
assert!(cache_json.get("last_updated").is_some());
}
#[test]
fn test_directory_cache_not_created_by_default() {
let (source, dest) = setup_test_dir("cache_default");
fs::write(source.path().join("file.txt"), "content").unwrap();
let output = Command::new(sy_bin()).args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap()]).output().unwrap();
assert!(output.status.success());
let cache_file = dest.path().join(".sy-dir-cache.json");
assert!(!cache_file.exists(), "Directory cache should not be created without --use-cache flag");
}
#[test]
fn test_directory_cache_persists() {
let (source, dest) = setup_test_dir("cache_persist");
fs::create_dir_all(source.path().join("a/b/c")).unwrap();
fs::write(source.path().join("a/file1.txt"), "content1").unwrap();
fs::write(source.path().join("a/b/file2.txt"), "content2").unwrap();
fs::write(source.path().join("a/b/c/file3.txt"), "content3").unwrap();
Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--use-cache=true"])
.output()
.unwrap();
let cache_file = dest.path().join(".sy-dir-cache.json");
assert!(cache_file.exists());
let initial_cache = fs::read_to_string(&cache_file).unwrap();
let initial_json: serde_json::Value = serde_json::from_str(&initial_cache).unwrap();
let initial_dirs = initial_json["directories"].as_object().unwrap();
assert!(initial_dirs.len() >= 3, "Cache should contain at least 3 directories, found {}", initial_dirs.len());
std::thread::sleep(std::time::Duration::from_millis(100));
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--use-cache=true"])
.output()
.unwrap();
assert!(output.status.success());
assert!(cache_file.exists());
let second_cache = fs::read_to_string(&cache_file).unwrap();
let second_json: serde_json::Value = serde_json::from_str(&second_cache).unwrap();
assert!(second_json.get("directories").is_some());
assert!(second_json.get("version").is_some());
}
#[test]
fn test_directory_cache_clear() {
let (source, dest) = setup_test_dir("cache_clear");
fs::write(source.path().join("file.txt"), "content").unwrap();
Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--use-cache=true"])
.output()
.unwrap();
let cache_file = dest.path().join(".sy-dir-cache.json");
assert!(cache_file.exists(), "Cache should be created");
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--clear-cache"])
.output()
.unwrap();
assert!(output.status.success());
assert!(!cache_file.exists(), "Cache should be deleted after --clear-cache");
}
#[test]
fn test_directory_cache_dry_run() {
let (source, dest) = setup_test_dir("cache_dry_run");
fs::write(source.path().join("file.txt"), "content").unwrap();
let output = Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--use-cache=true", "--dry-run"])
.output()
.unwrap();
assert!(output.status.success());
let cache_file = dest.path().join(".sy-dir-cache.json");
assert!(!cache_file.exists(), "Cache should not be saved during dry-run");
}
#[test]
fn test_directory_cache_updates_on_new_directories() {
let (source, dest) = setup_test_dir("cache_updates");
fs::create_dir_all(source.path().join("dir1")).unwrap();
fs::write(source.path().join("dir1/file1.txt"), "content1").unwrap();
Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--use-cache=true"])
.output()
.unwrap();
let cache_file = dest.path().join(".sy-dir-cache.json");
let initial_cache = fs::read_to_string(&cache_file).unwrap();
let initial_json: serde_json::Value = serde_json::from_str(&initial_cache).unwrap();
let initial_count = initial_json["directories"].as_object().unwrap().len();
std::thread::sleep(std::time::Duration::from_secs(2));
fs::create_dir_all(source.path().join("dir2")).unwrap();
fs::create_dir_all(source.path().join("dir3/subdir")).unwrap();
fs::write(source.path().join("dir2/file2.txt"), "content2").unwrap();
fs::write(source.path().join("dir3/subdir/file3.txt"), "content3").unwrap();
Command::new(sy_bin())
.args([&format!("{}/", source.path().display()), dest.path().to_str().unwrap(), "--use-cache=true"])
.output()
.unwrap();
let updated_cache = fs::read_to_string(&cache_file).unwrap();
let updated_json: serde_json::Value = serde_json::from_str(&updated_cache).unwrap();
let updated_count = updated_json["directories"].as_object().unwrap().len();
assert!(
updated_count > initial_count,
"Cache should be updated with new directories. Initial: {}, Updated: {}",
initial_count,
updated_count
);
}