use std::fs; use std::io; #[cfg(unix)] use std::os::unix; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::path::PathBuf; use std::process::Command; use filetime::FileTime; use tempfile::TempDir; use rusync::progress::ProgressInfo; fn assert_same_contents(a: &Path, b: &Path) { assert!(a.exists(), "{:?} does not exist", a); assert!(b.exists(), "{:?} does not exist", b); let status = Command::new("diff") .args(&[a, b]) .status() .expect("Failed to execute process"); assert!(status.success(), "{:?} and {:?} differ", a, b) } #[cfg(unix)] fn is_executable(path: &Path) -> bool { let metadata = std::fs::metadata(&path) .unwrap_or_else(|e| panic!("Could not get metadata of {:?}: {}", path, e)); let permissions = metadata.permissions(); let mode = permissions.mode(); mode & 0o111 != 0 } #[cfg(unix)] fn assert_executable(path: &Path) { assert!( is_executable(path), "{:?} does not appear to be executable", path ); } #[cfg(unix)] fn assert_not_executable(path: &Path) { assert!(!is_executable(path), "{:?} appears to be executable", path); } fn setup_test(tmp_path: &Path) -> (PathBuf, PathBuf) { let src_path = tmp_path.join("src"); let dest_path = tmp_path.join("dest"); let status = Command::new("cp") .args(&["-R", "tests/data", &src_path.to_string_lossy()]) .status() .expect("Failed to start cp process"); assert!(status.success(), "could not copy test data"); (src_path, dest_path) } fn make_recent(path: &Path) -> io::Result<()> { let metadata = fs::metadata(&path)?; let atime = FileTime::from_last_access_time(&metadata); let mtime = FileTime::from_last_modification_time(&metadata); let mut epoch = mtime.unix_seconds(); epoch += 1; let mtime = FileTime::from_unix_time(epoch, 0); filetime::set_file_times(&path, atime, mtime)?; Ok(()) } struct DummyProgressInfo {} impl ProgressInfo for DummyProgressInfo {} fn new_test_syncer(src: &Path, dest: &Path) -> rusync::Syncer { let dummy_progress_info = DummyProgressInfo {}; let options = rusync::SyncOptions { preserve_permissions: true, }; rusync::Syncer::new(src, dest, options, Box::new(dummy_progress_info)) } #[test] fn fresh_copy() -> Result<(), std::io::Error> { let tmp_dir = TempDir::new()?; let (src_path, dest_path) = setup_test(tmp_dir.path()); let syncer = new_test_syncer(&src_path, &dest_path); let outcome = syncer.sync(); assert!(outcome.is_ok()); let src_top = src_path.join("top.txt"); let dest_top = dest_path.join("top.txt"); assert_same_contents(&src_top, &dest_top); Ok(()) } #[test] fn skip_up_to_date_files() -> Result<(), std::io::Error> { let tmp_dir = TempDir::new()?; let (src_path, dest_path) = setup_test(tmp_dir.path()); let syncer = new_test_syncer(&src_path, &dest_path); let stats = syncer.sync().unwrap(); assert_eq!(stats.up_to_date, 0); let src_top_txt = src_path.join("top.txt"); make_recent(&src_top_txt)?; let syncer = new_test_syncer(&src_path, &dest_path); let stats = syncer.sync().unwrap(); assert_eq!(stats.copied, 1); Ok(()) } #[test] #[cfg(unix)] fn preserve_permissions() -> Result<(), std::io::Error> { let tmp_dir = TempDir::new()?; let (src_path, dest_path) = setup_test(tmp_dir.path()); let syncer = new_test_syncer(&src_path, &dest_path); syncer.sync().unwrap(); let dest_exe = &dest_path.join("a_dir/foo.exe"); assert_executable(dest_exe); Ok(()) } #[test] #[cfg(unix)] fn do_not_preserve_permissions() -> Result<(), std::io::Error> { let tmp_dir = TempDir::new()?; let (src_path, dest_path) = setup_test(tmp_dir.path()); let options = rusync::SyncOptions { preserve_permissions: false, }; let syncer = rusync::Syncer::new( &src_path, &dest_path, options, Box::new(DummyProgressInfo {}), ); syncer.sync().unwrap(); let dest_exe = &dest_path.join("a_dir/foo.exe"); assert_not_executable(dest_exe); Ok(()) } #[test] fn rewrite_partially_written_files() -> Result<(), std::io::Error> { let tmp_dir = TempDir::new()?; let (src_path, dest_path) = setup_test(tmp_dir.path()); let src_top = src_path.join("top.txt"); let expected = fs::read_to_string(&src_top)?; let syncer = new_test_syncer(&src_path, &dest_path); syncer.sync().unwrap(); let dest_top = dest_path.join("top.txt"); // Corrupt the dest/top.txt fs::write(&dest_top, "this is")?; let syncer = new_test_syncer(&src_path, &dest_path); syncer.sync().unwrap(); let actual = fs::read_to_string(&dest_top)?; assert_eq!(actual, expected); Ok(()) } #[test] fn dest_read_only() -> Result<(), std::io::Error> { let tmp_dir = TempDir::new()?; let (src_path, dest_path) = setup_test(tmp_dir.path()); fs::create_dir_all(&dest_path)?; let dest_top = dest_path.join("top.txt"); fs::write(&dest_top, "this is read only")?; let mut perms = fs::metadata(&dest_top)?.permissions(); perms.set_readonly(true); fs::set_permissions(&dest_top, perms)?; let src_top = src_path.join("top.txt"); make_recent(&src_top)?; let syncer = new_test_syncer(&src_path, &dest_path); let result = syncer.sync().unwrap(); assert_eq!(result.errors, 1); Ok(()) } #[test] #[cfg(unix)] fn broken_link_in_src() -> Result<(), std::io::Error> { let tmp_dir = TempDir::new()?; let (src_path, dest_path) = setup_test(tmp_dir.path()); let src_broken_link = &src_path.join("broken"); unix::fs::symlink("no-such", &src_broken_link)?; let syncer = new_test_syncer(&src_path, &dest_path); let result = syncer.sync(); let dest_broken_link = &dest_path.join("broken"); assert!(!dest_broken_link.exists()); assert_eq!(dest_broken_link.read_link()?.to_string_lossy(), "no-such"); assert!(result.is_ok()); Ok(()) }