use super::{ BackingRoot, Intent, Mode, ObjectStamp, Provenance, copy_tree, initialize, migrate, path_bytes, persist_intent, scan_tree, }; #[test] fn runtime_migration_refuses_multiply_linked_regular_files() { let fixture = Fixture::new("runtime-hardlink"); let host = BackingRoot::open(&fixture.host).unwrap(); let isolated = BackingRoot::open(&fixture.isolated).unwrap(); initialize(&host, &isolated).unwrap(); let source = std::path::Path::new("temporary.bin"); let peer = std::path::Path::new("peer.bin"); host.write_file(source, b"shared", Mode::from_raw_mode(0o600)) .unwrap(); host.link(source, peer).unwrap(); let mut provenance = Provenance::default(); provenance.record(source.to_path_buf(), &host.stat(source).unwrap()); let error = migrate( &host, &isolated, &provenance, source, std::path::Path::new("isolated.bin"), true, ) .unwrap_err(); assert_eq!(error.raw_os_error(), Some(libc::EXDEV)); assert_eq!( host.stat(source).unwrap().st_ino, host.stat(peer).unwrap().st_ino ); assert!(isolated.stat(std::path::Path::new("isolated.bin")).is_err()); } #[test] fn startup_restores_replaced_destination_before_publication() { let fixture = Fixture::new("replacement-rollback"); let host = BackingRoot::open(&fixture.host).unwrap(); let isolated = BackingRoot::open(&fixture.isolated).unwrap(); initialize(&host, &isolated).unwrap(); let source = std::path::Path::new("temporary.rdb"); let destination = std::path::Path::new("dump.rdb"); let quarantine = std::path::Path::new(".lazaret-quarantine-rollback"); let staging = std::path::Path::new(".lazaret-staging-rollback"); let backup = std::path::Path::new(".lazaret-backup-rollback"); host.write_file(source, b"new", Mode::from_raw_mode(0o600)) .unwrap(); isolated .write_file(destination, b"old", Mode::from_raw_mode(0o600)) .unwrap(); let intent = Intent { schema: 2, id: "rollback".to_owned(), source: path_bytes(source), destination: path_bytes(destination), quarantine: path_bytes(quarantine), staging: path_bytes(staging), backup: path_bytes(backup), destination_existed: true, destination_stamp: Some(ObjectStamp::from_stat(&isolated.stat(destination).unwrap())), manifest: scan_tree(&host, source).unwrap(), }; persist_intent(&isolated, &intent).unwrap(); host.rename(source, quarantine, rustix::fs::RenameFlags::NOREPLACE) .unwrap(); host.sync_directory(std::path::Path::new("")).unwrap(); copy_tree(&host, quarantine, &isolated, staging).unwrap(); isolated.sync_directory(std::path::Path::new("")).unwrap(); isolated .rename(destination, backup, rustix::fs::RenameFlags::NOREPLACE) .unwrap(); isolated.sync_directory(std::path::Path::new("")).unwrap(); initialize(&host, &isolated).unwrap(); assert_eq!(host.read_file(source, 16).unwrap(), b"new"); assert_eq!(isolated.read_file(destination, 16).unwrap(), b"old"); assert!(isolated.stat(staging).is_err()); assert!(isolated.stat(backup).is_err()); } #[test] fn startup_completes_published_replacement() { let fixture = Fixture::new("replacement-published"); let host = BackingRoot::open(&fixture.host).unwrap(); let isolated = BackingRoot::open(&fixture.isolated).unwrap(); initialize(&host, &isolated).unwrap(); let source = std::path::Path::new("temporary.rdb"); let destination = std::path::Path::new("dump.rdb"); let quarantine = std::path::Path::new(".lazaret-quarantine-published"); let staging = std::path::Path::new(".lazaret-staging-published"); let backup = std::path::Path::new(".lazaret-backup-published"); host.write_file(source, b"new", Mode::from_raw_mode(0o600)) .unwrap(); isolated .write_file(destination, b"old", Mode::from_raw_mode(0o600)) .unwrap(); let intent = Intent { schema: 2, id: "published".to_owned(), source: path_bytes(source), destination: path_bytes(destination), quarantine: path_bytes(quarantine), staging: path_bytes(staging), backup: path_bytes(backup), destination_existed: true, destination_stamp: Some(ObjectStamp::from_stat(&isolated.stat(destination).unwrap())), manifest: scan_tree(&host, source).unwrap(), }; persist_intent(&isolated, &intent).unwrap(); host.rename(source, quarantine, rustix::fs::RenameFlags::NOREPLACE) .unwrap(); host.sync_directory(std::path::Path::new("")).unwrap(); copy_tree(&host, quarantine, &isolated, staging).unwrap(); isolated.sync_directory(std::path::Path::new("")).unwrap(); isolated .rename(destination, backup, rustix::fs::RenameFlags::NOREPLACE) .unwrap(); isolated.sync_directory(std::path::Path::new("")).unwrap(); isolated .rename(staging, destination, rustix::fs::RenameFlags::NOREPLACE) .unwrap(); isolated.sync_directory(std::path::Path::new("")).unwrap(); initialize(&host, &isolated).unwrap(); assert!(host.stat(source).is_err()); assert!(host.stat(quarantine).is_err()); assert!(isolated.stat(backup).is_err()); assert_eq!(isolated.read_file(destination, 16).unwrap(), b"new"); } struct Fixture { root: std::path::PathBuf, host: std::path::PathBuf, isolated: std::path::PathBuf, } impl Fixture { fn new(name: &str) -> Self { let root = std::env::current_dir() .unwrap() .join("target/ignored-workspace-tests") .join(format!("migration-recovery-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&root); let host = root.join("host"); let isolated = root.join("isolated"); std::fs::create_dir_all(&host).unwrap(); std::fs::create_dir_all(&isolated).unwrap(); Self { root, host, isolated, } } } impl Drop for Fixture { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.root); } }