msy 0.4.7

Modern musl rsync alternative - Fast, parallel file synchronization
Documentation
// Hard link handling tests
//
// Tests verify:
// - Hard link detection and metadata tracking
// - Hard link preservation during sync
// - Hard link conflict detection in bisync
// - Proper handling of hard links in different scenarios

use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use tempfile::TempDir;

/// Create a test file and return its path
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)
}

/// Get inode number of a file (Unix only)
#[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())
}

/// Get number of hard links to a file (Unix only)
#[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 hard link creation and detection
#[test]
#[cfg(unix)]
fn test_hardlink_creation() {
	let temp_dir = TempDir::new().unwrap();

	// Create original file
	let file1 = create_file(temp_dir.path(), "file1.txt", "test content").unwrap();

	// Create hard link
	let file2 = temp_dir.path().join("file2.txt");
	fs::hard_link(&file1, &file2).unwrap();

	// Both files should have the same inode
	let inode1 = get_inode(&file1).unwrap();
	let inode2 = get_inode(&file2).unwrap();
	assert_eq!(inode1, inode2, "Hard links should have same inode");

	// Both files should report nlink=2
	assert_eq!(get_nlink(&file1).unwrap(), 2);
	assert_eq!(get_nlink(&file2).unwrap(), 2);

	// Content should be identical
	let content1 = fs::read_to_string(&file1).unwrap();
	let content2 = fs::read_to_string(&file2).unwrap();
	assert_eq!(content1, content2);

	// Modifying one should modify the other
	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 hard link set (3+ files linked together)
#[test]
#[cfg(unix)]
fn test_hardlink_set() {
	let temp_dir = TempDir::new().unwrap();

	// Create original file
	let file1 = create_file(temp_dir.path(), "file1.txt", "shared").unwrap();

	// Create multiple hard links
	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();

	// All should have same inode
	let inode1 = get_inode(&file1).unwrap();
	assert_eq!(get_inode(&file2).unwrap(), inode1);
	assert_eq!(get_inode(&file3).unwrap(), inode1);

	// All should report nlink=3
	assert_eq!(get_nlink(&file1).unwrap(), 3);
	assert_eq!(get_nlink(&file2).unwrap(), 3);
	assert_eq!(get_nlink(&file3).unwrap(), 3);
}

/// Test sy sync preserves hard links (basic single-direction sync)
#[test]
#[cfg(unix)]
#[ignore] // Slow test - run explicitly with --ignored
fn test_sync_preserves_hardlinks() {
	let source_dir = TempDir::new().unwrap();
	let dest_dir = TempDir::new().unwrap();

	// Create file and hard link in source
	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);

	// Sync using sy
	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));

	// Check if destination preserves hard links
	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();

	// NOTE: Current behavior check - document what sy actually does
	// If hard links are preserved: dest_inode1 == dest_inode2 and nlink == 2
	// If hard links are NOT preserved: dest_inode1 != dest_inode2 and nlink == 1

	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);

		// This is current behavior - hard links become independent files
		// Future enhancement: preserve hard link relationships
	}
}

/// Test bisync handles hard links correctly
#[test]
#[cfg(unix)]
#[ignore] // Slow test - run explicitly with --ignored
fn test_bisync_with_hardlinks() {
	let source_dir = TempDir::new().unwrap();
	let dest_dir = TempDir::new().unwrap();

	// Create hard link set in source
	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();

	// First sync (initial)
	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");

	// Second sync (should be idempotent)
	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);

	// Idempotent sync should show no changes
	// (hard links shouldn't trigger false positives)
	assert!(!stdout.contains("conflict"), "Hard links shouldn't cause conflicts");

	println!("✅ Bisync handles hard links without false conflicts");
}

/// Test hard link conflict detection in bisync
#[test]
#[cfg(unix)]
#[ignore] // Slow test - run explicitly with --ignored
fn test_bisync_hardlink_conflict() {
	let source_dir = TempDir::new().unwrap();
	let dest_dir = TempDir::new().unwrap();

	// Create different hard link structures on each side
	// Source: file1 -> file2 (hard linked)
	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();

	// Dest: file1 and file2 are independent files
	create_file(dest_dir.path(), "file1.txt", "dest data 1").unwrap();
	create_file(dest_dir.path(), "file2.txt", "dest data 2").unwrap();

	// Run bisync
	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");

	// Should detect that files have different content
	let stderr = String::from_utf8_lossy(&output.stderr);
	let stdout = String::from_utf8_lossy(&output.stdout);

	println!("stdout: {}", stdout);
	println!("stderr: {}", stderr);

	// This documents current behavior - bisync should handle
	// the content difference (not specifically the hard link difference)
	println!("✅ Bisync detects content differences in hard link conflicts");
}

/// Test hard link preservation across directories
#[test]
#[cfg(unix)]
#[ignore] // Slow test - run explicitly with --ignored
fn test_hardlinks_across_directories() {
	let source_dir = TempDir::new().unwrap();
	let dest_dir = TempDir::new().unwrap();

	// Create subdirectories
	fs::create_dir(source_dir.path().join("dir1")).unwrap();
	fs::create_dir(source_dir.path().join("dir2")).unwrap();

	// Create file in dir1 and hard link in dir2
	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());

	// Sync using sy
	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");

	// Check preservation
	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 hard link handling with file modifications
#[test]
#[cfg(unix)]
#[ignore] // Slow test - run explicitly with --ignored
fn test_hardlink_modification_detection() {
	let source_dir = TempDir::new().unwrap();
	let dest_dir = TempDir::new().unwrap();

	// Create hard link in source
	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();

	// Initial sync
	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");

	// Modify one of the hard-linked files
	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);

	// Second sync should detect change
	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");

	// Both destination files should have updated content
	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");
}