165 lines
6.1 KiB
Rust
165 lines
6.1 KiB
Rust
|
|
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);
|
||
|
|
}
|
||
|
|
}
|