Preserve hardlink topology during startup routing
This commit is contained in:
parent
52c8c56e4f
commit
394fb65c86
1 changed files with 954 additions and 363 deletions
165
crates/lazaret-ignored-workspace/src/migration/recovery_tests.rs
Normal file
165
crates/lazaret-ignored-workspace/src/migration/recovery_tests.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue