use std::fs;
use std::io::Write;
use std::process::Command;
use tempfile::TempDir;
fn sy_bin() -> String {
env!("CARGO_BIN_EXE_sy").to_string()
}
#[test]
fn test_delta_sync_file_shrinks() {
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("test.dat");
fs::write(&dest_file, vec![0u8; 100_000]).unwrap();
let source_file = source.path().join("test.dat");
let source_data = vec![1u8; 50_000];
fs::write(&source_file, &source_data).unwrap();
let output = Command::new(sy_bin()).args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()]).output().unwrap();
assert!(output.status.success(), "Sync should succeed");
let result_data = fs::read(&dest_file).unwrap();
assert_eq!(result_data.len(), 50_000, "Dest file should be truncated to source size");
assert_eq!(result_data, source_data, "Dest file should match source exactly");
}
#[test]
fn test_delta_sync_file_grows() {
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("test.dat");
fs::write(&dest_file, vec![0u8; 50_000]).unwrap();
let source_file = source.path().join("test.dat");
let source_data = vec![1u8; 100_000];
fs::write(&source_file, &source_data).unwrap();
let output = Command::new(sy_bin()).args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()]).output().unwrap();
assert!(output.status.success(), "Sync should succeed");
let result_data = fs::read(&dest_file).unwrap();
assert_eq!(result_data.len(), 100_000, "Dest file should grow to source size");
assert_eq!(result_data, source_data, "Dest file should match source exactly");
}
#[test]
fn test_delta_sync_correctness() {
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("test.dat");
let mut initial_data = Vec::new();
for i in 0..10_000 {
writeln!(&mut initial_data, "block {:04}", i).unwrap();
}
fs::write(&dest_file, &initial_data).unwrap();
let source_file = source.path().join("test.dat");
let mut modified_data = initial_data.clone();
for i in 100..200 {
let offset = i * 11; let replacement = format!("CHANG {:04}\n", i);
modified_data[offset..offset + 11].copy_from_slice(replacement.as_bytes());
}
fs::write(&source_file, &modified_data).unwrap();
let output = Command::new(sy_bin()).args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()]).output().unwrap();
assert!(output.status.success(), "Sync should succeed");
let result_data = fs::read(&dest_file).unwrap();
assert_eq!(result_data, modified_data, "Dest file should be bit-identical to source after delta sync");
}
#[test]
#[cfg(unix)]
fn test_hard_links_preserved() {
use std::os::unix::fs::MetadataExt;
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let file1 = source.path().join("file1.txt");
fs::write(&file1, "shared content").unwrap();
let file2 = source.path().join("file2.txt");
fs::hard_link(&file1, &file2).unwrap();
let inode1 = fs::metadata(&file1).unwrap().ino();
let inode2 = fs::metadata(&file2).unwrap().ino();
assert_eq!(inode1, inode2, "Source files should be hard linked");
let source_path = format!("{}/", source.path().display());
let output = Command::new(sy_bin()).args([&source_path, dest.path().to_str().unwrap(), "--preserve-hardlinks"]).output().unwrap();
assert!(output.status.success(), "Sync should succeed");
let dest_file1 = dest.path().join("file1.txt");
let dest_file2 = dest.path().join("file2.txt");
assert!(dest_file1.exists());
assert!(dest_file2.exists());
let dest_inode1 = fs::metadata(&dest_file1).unwrap().ino();
let dest_inode2 = fs::metadata(&dest_file2).unwrap().ino();
assert_eq!(dest_inode1, dest_inode2, "Dest files should be hard linked (same inode)");
assert_eq!(fs::read_to_string(&dest_file1).unwrap(), "shared content");
assert_eq!(fs::read_to_string(&dest_file2).unwrap(), "shared content");
}
#[test]
#[cfg(unix)]
fn test_hard_link_update_both_files_same_content() {
use std::os::unix::fs::MetadataExt;
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let file1 = source.path().join("file1.txt");
let file2 = source.path().join("file2.txt");
fs::write(&file1, "initial").unwrap();
fs::hard_link(&file1, &file2).unwrap();
let source_path = format!("{}/", source.path().display());
Command::new(sy_bin()).args([&source_path, dest.path().to_str().unwrap(), "--preserve-hardlinks"]).output().unwrap();
fs::write(&file1, "modified content").unwrap();
let output = Command::new(sy_bin()).args([&source_path, dest.path().to_str().unwrap(), "--preserve-hardlinks"]).output().unwrap();
assert!(output.status.success(), "Sync should succeed");
let dest_file1 = dest.path().join("file1.txt");
let dest_file2 = dest.path().join("file2.txt");
assert_eq!(fs::read_to_string(&dest_file1).unwrap(), "modified content");
assert_eq!(fs::read_to_string(&dest_file2).unwrap(), "modified content");
let dest_inode1 = fs::metadata(&dest_file1).unwrap().ino();
let dest_inode2 = fs::metadata(&dest_file2).unwrap().ino();
assert_eq!(dest_inode1, dest_inode2, "Hard link should be preserved");
}
#[test]
#[cfg(target_os = "macos")]
fn test_cow_strategy_used_on_apfs() {
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("test.dat");
fs::write(&dest_file, vec![0u8; 15_000_000]).unwrap();
let source_file = source.path().join("test.dat");
let mut source_data = vec![0u8; 15_000_000];
source_data[5_000_000] = 1; fs::write(&source_file, &source_data).unwrap();
let output = Command::new(sy_bin())
.args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()])
.env("RUST_LOG", "sy=info")
.output()
.unwrap();
assert!(output.status.success(), "Sync should succeed");
let result_data = fs::read(&dest_file).unwrap();
assert_eq!(result_data, source_data, "Dest should match source");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let cow_used = stderr.contains("COW (clone + selective writes)") || stdout.contains("COW (clone + selective writes)");
let no_cow_support = stderr.contains("filesystem does not support COW reflinks") || stdout.contains("filesystem does not support COW reflinks");
assert!(
cow_used || no_cow_support,
"Should use COW strategy on APFS or report no COW support. Stderr: {}\nStdout: {}",
stderr,
stdout
);
}
#[test]
#[cfg(unix)]
fn test_inplace_strategy_used_with_hard_links() {
use std::os::unix::fs::MetadataExt;
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("test.dat");
let dest_link = dest.path().join("test_link.dat");
fs::write(&dest_file, vec![0u8; 15_000_000]).unwrap();
fs::hard_link(&dest_file, &dest_link).unwrap();
let nlink = fs::metadata(&dest_file).unwrap().nlink();
assert_eq!(nlink, 2, "Dest file should have hard link");
let source_file = source.path().join("test.dat");
let mut source_data = vec![0u8; 15_000_000];
for byte in &mut source_data[..3_750_000] {
*byte = 1;
}
fs::write(&source_file, &source_data).unwrap();
let output = Command::new(sy_bin())
.args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()])
.env("RUST_LOG", "sy=info")
.output()
.unwrap();
assert!(output.status.success(), "Sync should succeed");
let result_data = fs::read(&dest_file).unwrap();
assert_eq!(result_data, source_data, "Dest should match source");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stderr.contains("in-place (full file rebuild)") || stdout.contains("in-place (full file rebuild)"),
"Should use in-place strategy (for hard links or filesystem limitations). Stderr: {}\nStdout: {}",
stderr,
stdout
);
}
#[test]
fn test_both_strategies_produce_identical_results() {
let initial_data = vec![0u8; 100_000];
let mut modified_data = initial_data.clone();
for byte in &mut modified_data[45_000..55_000] {
*byte = 0xFF;
}
{
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("test.dat");
fs::write(&dest_file, &initial_data).unwrap();
let source_file = source.path().join("test.dat");
fs::write(&source_file, &modified_data).unwrap();
let output = Command::new(sy_bin()).args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()]).output().unwrap();
assert!(output.status.success());
let result1 = fs::read(&dest_file).unwrap();
assert_eq!(result1, modified_data, "COW strategy should produce correct output");
}
#[cfg(unix)]
{
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("test.dat");
let dest_link = dest.path().join("link.dat");
fs::write(&dest_file, &initial_data).unwrap();
fs::hard_link(&dest_file, &dest_link).unwrap();
let source_file = source.path().join("test.dat");
fs::write(&source_file, &modified_data).unwrap();
let output = Command::new(sy_bin()).args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()]).output().unwrap();
assert!(output.status.success());
let result2 = fs::read(&dest_file).unwrap();
assert_eq!(result2, modified_data, "In-place strategy should produce correct output");
}
}
#[test]
fn test_strategy_selection_correctness() {
let test_sizes = vec![
1_000, 10_000, 100_000, 1_000_000, ];
for size in test_sizes {
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("test.dat");
fs::write(&dest_file, vec![0u8; size]).unwrap();
let source_file = source.path().join("test.dat");
let source_data: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
fs::write(&source_file, &source_data).unwrap();
let output = Command::new(sy_bin()).args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()]).output().unwrap();
assert!(output.status.success(), "Sync should succeed for size {}", size);
let result_data = fs::read(&dest_file).unwrap();
assert_eq!(result_data, source_data, "Dest should match source exactly for size {}", size);
}
}
#[test]
#[cfg(unix)]
#[ignore] fn test_cross_filesystem_uses_inplace_strategy() {
use std::env;
let cross_fs_path = env::var("CROSS_FS_PATH").expect("CROSS_FS_PATH not set - see test comments for setup instructions");
let source = TempDir::new().unwrap();
let cross_fs_dest = std::path::PathBuf::from(&cross_fs_path);
let dest_file = cross_fs_dest.join("test_cross_fs.dat");
fs::write(&dest_file, vec![0u8; 15_000_000]).unwrap();
let source_file = source.path().join("test.dat");
let source_data = vec![1u8; 15_000_000];
fs::write(&source_file, &source_data).unwrap();
let output = Command::new(sy_bin())
.args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()])
.env("RUST_LOG", "sy=info")
.output()
.unwrap();
assert!(output.status.success(), "Sync should succeed");
let result_data = fs::read(&dest_file).unwrap();
assert_eq!(result_data, source_data, "Dest should match source");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
(stderr.contains("in-place (full file rebuild)") && stderr.contains("different filesystems"))
|| (stdout.contains("in-place (full file rebuild)") && stdout.contains("different filesystems")),
"Should use in-place strategy for cross-filesystem. Stderr: {}\nStdout: {}",
stderr,
stdout
);
let _ = fs::remove_file(&dest_file);
}
#[test]
#[cfg(unix)]
fn test_same_filesystem_detection() {
use msy::fs_util::same_filesystem;
let temp = TempDir::new().unwrap();
let file1 = temp.path().join("file1.txt");
let file2 = temp.path().join("file2.txt");
fs::write(&file1, b"test1").unwrap();
fs::write(&file2, b"test2").unwrap();
assert!(same_filesystem(&file1, &file2), "Files in same directory should be on same filesystem");
assert!(same_filesystem(&file1, temp.path()), "File and parent directory should be on same filesystem");
}
#[test]
#[cfg(unix)]
fn test_sparse_file_delta_sync_preserves_sparseness() {
use std::io::{Seek, SeekFrom, Write};
use std::os::unix::fs::MetadataExt;
let source = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_file = dest.path().join("sparse.dat");
{
let mut f = fs::File::create(&dest_file).unwrap();
f.write_all(&vec![0xAA; 1_000_000]).unwrap(); f.seek(SeekFrom::Current(8_000_000)).unwrap(); f.write_all(&vec![0xBB; 1_000_000]).unwrap(); f.sync_all().unwrap();
}
let dest_meta = fs::metadata(&dest_file).unwrap();
if dest_meta.blocks() * 512 >= dest_meta.len() {
eprintln!(
"Filesystem doesn't support sparse files (allocated {} >= logical {}), skipping test",
dest_meta.blocks() * 512,
dest_meta.len()
);
return;
}
assert_eq!(dest_meta.len(), 10_000_000, "Dest logical size should be 10MB");
let source_file = source.path().join("sparse.dat");
{
let mut f = fs::File::create(&source_file).unwrap();
f.write_all(&vec![0xCC; 1_000_000]).unwrap(); f.seek(SeekFrom::Current(8_000_000)).unwrap(); f.write_all(&vec![0xDD; 1_000_000]).unwrap(); f.sync_all().unwrap();
}
let source_meta = fs::metadata(&source_file).unwrap();
assert_eq!(source_meta.len(), 10_000_000, "Source logical size should be 10MB");
assert!(source_meta.blocks() * 512 < source_meta.len(), "Source should be sparse (allocated < logical)");
let output = Command::new(sy_bin())
.args([source_file.to_str().unwrap(), dest_file.to_str().unwrap()])
.env("RUST_LOG", "sy=info")
.output()
.unwrap();
assert!(output.status.success(), "Sync should succeed for sparse file");
let result_meta = fs::metadata(&dest_file).unwrap();
assert_eq!(result_meta.len(), 10_000_000, "Result logical size should be 10MB");
let source_data = fs::read(&source_file).unwrap();
let dest_data = fs::read(&dest_file).unwrap();
assert_eq!(dest_data, source_data, "Dest content should match source exactly");
let result_allocated = result_meta.blocks() * 512;
if result_allocated < result_meta.len() {
eprintln!("✓ Sparseness preserved: {} allocated vs {} logical", result_allocated, result_meta.len());
} else {
eprintln!(
"⚠ Sparseness not preserved on this filesystem: {} allocated vs {} logical\n\
This is expected on some filesystems (e.g., ext4 with older kernels, some CI environments)",
result_allocated,
result_meta.len()
);
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stderr.contains("sparse") || stdout.contains("sparse"),
"Should log sparse file detection. Stderr: {}\nStdout: {}",
stderr,
stdout
);
}