lazaret/crates/lazaret-ignored-workspace/src/migration/recovery_tests.rs

165 lines
6.1 KiB
Rust
Raw Normal View History

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