use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use tempfile::TempDir;
fn create_file(dir: &Path, name: &str, content: &str) -> std::io::Result<std::path::PathBuf> {
let path = dir.join(name);
let mut file = File::create(&path)?;
write!(file, "{}", content)?;
Ok(path)
}
#[cfg(unix)]
fn get_inode(path: &Path) -> std::io::Result<u64> {
use std::os::unix::fs::MetadataExt;
let metadata = fs::metadata(path)?;
Ok(metadata.ino())
}
#[cfg(unix)]
fn get_nlink(path: &Path) -> std::io::Result<u64> {
use std::os::unix::fs::MetadataExt;
let metadata = fs::metadata(path)?;
Ok(metadata.nlink())
}
#[test]
#[cfg(unix)]
fn test_hardlink_creation() {
let temp_dir = TempDir::new().unwrap();
let file1 = create_file(temp_dir.path(), "file1.txt", "test content").unwrap();
let file2 = temp_dir.path().join("file2.txt");
fs::hard_link(&file1, &file2).unwrap();
let inode1 = get_inode(&file1).unwrap();
let inode2 = get_inode(&file2).unwrap();
assert_eq!(inode1, inode2, "Hard links should have same inode");
assert_eq!(get_nlink(&file1).unwrap(), 2);
assert_eq!(get_nlink(&file2).unwrap(), 2);
let content1 = fs::read_to_string(&file1).unwrap();
let content2 = fs::read_to_string(&file2).unwrap();
assert_eq!(content1, content2);
let mut file = fs::OpenOptions::new().append(true).open(&file1).unwrap();
write!(file, " appended").unwrap();
drop(file);
let updated_content = fs::read_to_string(&file2).unwrap();
assert_eq!(updated_content, "test content appended");
}
#[test]
#[cfg(unix)]
fn test_hardlink_set() {
let temp_dir = TempDir::new().unwrap();
let file1 = create_file(temp_dir.path(), "file1.txt", "shared").unwrap();
let file2 = temp_dir.path().join("file2.txt");
let file3 = temp_dir.path().join("file3.txt");
fs::hard_link(&file1, &file2).unwrap();
fs::hard_link(&file1, &file3).unwrap();
let inode1 = get_inode(&file1).unwrap();
assert_eq!(get_inode(&file2).unwrap(), inode1);
assert_eq!(get_inode(&file3).unwrap(), inode1);
assert_eq!(get_nlink(&file1).unwrap(), 3);
assert_eq!(get_nlink(&file2).unwrap(), 3);
assert_eq!(get_nlink(&file3).unwrap(), 3);
}
#[test]
#[cfg(unix)]
#[ignore] fn test_sync_preserves_hardlinks() {
let source_dir = TempDir::new().unwrap();
let dest_dir = TempDir::new().unwrap();
let file1 = create_file(source_dir.path(), "original.txt", "data").unwrap();
let file2 = source_dir.path().join("link.txt");
fs::hard_link(&file1, &file2).unwrap();
let _source_inode = get_inode(&file1).unwrap();
assert_eq!(get_nlink(&file1).unwrap(), 2);
let output = std::process::Command::new(env!("CARGO_BIN_EXE_sy"))
.arg(source_dir.path())
.arg(dest_dir.path())
.output()
.expect("Failed to execute sy");
assert!(output.status.success(), "Sync failed: {:?}", String::from_utf8_lossy(&output.stderr));
let dest_file1 = dest_dir.path().join("original.txt");
let dest_file2 = dest_dir.path().join("link.txt");
assert!(dest_file1.exists());
assert!(dest_file2.exists());
let dest_inode1 = get_inode(&dest_file1).unwrap();
let dest_inode2 = get_inode(&dest_file2).unwrap();
if dest_inode1 == dest_inode2 {
println!("✅ Hard links preserved: both files share inode {}", dest_inode1);
assert_eq!(get_nlink(&dest_file1).unwrap(), 2);
assert_eq!(get_nlink(&dest_file2).unwrap(), 2);
} else {
println!("⚠️ Hard links NOT preserved: files copied independently");
println!(" original.txt inode: {}", dest_inode1);
println!(" link.txt inode: {}", dest_inode2);
assert_eq!(get_nlink(&dest_file1).unwrap(), 1);
assert_eq!(get_nlink(&dest_file2).unwrap(), 1);
}
}
#[test]
#[cfg(unix)]
#[ignore] fn test_bisync_with_hardlinks() {
let source_dir = TempDir::new().unwrap();
let dest_dir = TempDir::new().unwrap();
let file1 = create_file(source_dir.path(), "data1.txt", "content").unwrap();
let file2 = source_dir.path().join("data2.txt");
fs::hard_link(&file1, &file2).unwrap();
let output = std::process::Command::new(env!("CARGO_BIN_EXE_sy"))
.arg(source_dir.path())
.arg(dest_dir.path())
.arg("--bidirectional")
.output()
.expect("Failed to execute sy");
assert!(output.status.success(), "Initial bisync failed");
let output = std::process::Command::new(env!("CARGO_BIN_EXE_sy"))
.arg(source_dir.path())
.arg(dest_dir.path())
.arg("--bidirectional")
.output()
.expect("Failed to execute sy");
assert!(output.status.success(), "Second bisync failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!stdout.contains("conflict"), "Hard links shouldn't cause conflicts");
println!("✅ Bisync handles hard links without false conflicts");
}
#[test]
#[cfg(unix)]
#[ignore] fn test_bisync_hardlink_conflict() {
let source_dir = TempDir::new().unwrap();
let dest_dir = TempDir::new().unwrap();
let source_file1 = create_file(source_dir.path(), "file1.txt", "source data").unwrap();
let source_file2 = source_dir.path().join("file2.txt");
fs::hard_link(&source_file1, &source_file2).unwrap();
create_file(dest_dir.path(), "file1.txt", "dest data 1").unwrap();
create_file(dest_dir.path(), "file2.txt", "dest data 2").unwrap();
let output = std::process::Command::new(env!("CARGO_BIN_EXE_sy"))
.arg(source_dir.path())
.arg(dest_dir.path())
.arg("--bidirectional")
.output()
.expect("Failed to execute sy");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
println!("stdout: {}", stdout);
println!("stderr: {}", stderr);
println!("✅ Bisync detects content differences in hard link conflicts");
}
#[test]
#[cfg(unix)]
#[ignore] fn test_hardlinks_across_directories() {
let source_dir = TempDir::new().unwrap();
let dest_dir = TempDir::new().unwrap();
fs::create_dir(source_dir.path().join("dir1")).unwrap();
fs::create_dir(source_dir.path().join("dir2")).unwrap();
let file1 = create_file(source_dir.path().join("dir1").as_path(), "data.txt", "shared").unwrap();
let file2 = source_dir.path().join("dir2").join("data.txt");
fs::hard_link(&file1, &file2).unwrap();
assert_eq!(get_inode(&file1).unwrap(), get_inode(&file2).unwrap());
let output = std::process::Command::new(env!("CARGO_BIN_EXE_sy"))
.arg(source_dir.path())
.arg(dest_dir.path())
.output()
.expect("Failed to execute sy");
assert!(output.status.success(), "Sync failed");
let dest_file1 = dest_dir.path().join("dir1").join("data.txt");
let dest_file2 = dest_dir.path().join("dir2").join("data.txt");
assert!(dest_file1.exists());
assert!(dest_file2.exists());
let dest_inode1 = get_inode(&dest_file1).unwrap();
let dest_inode2 = get_inode(&dest_file2).unwrap();
if dest_inode1 == dest_inode2 {
println!("✅ Hard links preserved across directories");
} else {
println!("⚠️ Hard links across directories not preserved");
}
}
#[test]
#[cfg(unix)]
#[ignore] fn test_hardlink_modification_detection() {
let source_dir = TempDir::new().unwrap();
let dest_dir = TempDir::new().unwrap();
let file1 = create_file(source_dir.path(), "original.txt", "initial").unwrap();
let file2 = source_dir.path().join("link.txt");
fs::hard_link(&file1, &file2).unwrap();
let output = std::process::Command::new(env!("CARGO_BIN_EXE_sy"))
.arg(source_dir.path())
.arg(dest_dir.path())
.arg("--bidirectional")
.output()
.expect("Failed to execute sy");
assert!(output.status.success(), "Initial sync failed");
std::thread::sleep(std::time::Duration::from_millis(10));
let mut file = fs::OpenOptions::new().write(true).truncate(true).open(&file1).unwrap();
write!(file, "modified content").unwrap();
drop(file);
let output = std::process::Command::new(env!("CARGO_BIN_EXE_sy"))
.arg(source_dir.path())
.arg(dest_dir.path())
.arg("--bidirectional")
.output()
.expect("Failed to execute sy");
assert!(output.status.success(), "Second sync failed");
let dest_content1 = fs::read_to_string(dest_dir.path().join("original.txt")).unwrap();
let dest_content2 = fs::read_to_string(dest_dir.path().join("link.txt")).unwrap();
assert_eq!(dest_content1, "modified content");
assert_eq!(dest_content2, "modified content");
println!("✅ Hard link modifications detected and synced correctly");
}