use super::*;
use crate::owner_lock::try_acquire_owner_lock;
use crate::owner_lock_record::{OwnerLockRecord, encode_owner_lock_record};
use crate::recovery_inspect::tests::owner_bytes;
use crate::recovery_landing::tests::Scene;
use crate::transaction_journal::LandingOperation;
use crate::transaction_registry::{RETIRED_SUFFIX, STAGING_SUFFIX};
const SECOND: &str = "1b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10";
const THIRD: &str = "2b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10";
fn add_entry(registry: &Path, name: &str, owner: Option<Vec<u8>>) -> PathBuf {
let directory: PathBuf = registry.join(name);
std::fs::create_dir_all(&directory).expect("entry");
if let Some(bytes) = owner {
std::fs::write(directory.join("owner.json"), bytes).expect("owner");
}
return directory;
}
fn action_for(outcomes: &[RecoveryOutcome], directory: &Path) -> Option<RecoveryAction> {
for outcome in outcomes {
if outcome.directory == directory {
return Some(outcome.action);
}
}
return None;
}
#[test]
fn every_entry_gets_its_outcome() {
let scene: Scene = Scene::new("recovery-scan-mixed", false);
scene.capture();
scene.land(LandingOperation::Commit, Vec::new());
let live: PathBuf = add_entry(
scene.registry.as_path(),
SECOND,
Some(owner_bytes(SECOND, "2026-10-06T00:00:01.000Z", true)),
);
let retained_name: String = format!("{THIRD}{STAGING_SUFFIX}");
let retained: PathBuf = add_entry(
scene.registry.as_path(),
retained_name.as_str(),
Some(owner_bytes(THIRD, "2026-10-06T00:00:02.000Z", false)),
);
let unattributed_name: String = format!("{SECOND}{STAGING_SUFFIX}");
let unattributed: PathBuf =
add_entry(scene.registry.as_path(), unattributed_name.as_str(), None);
let retired_name: String = format!("{THIRD}{RETIRED_SUFFIX}");
let retired: PathBuf = add_entry(scene.registry.as_path(), retired_name.as_str(), None);
std::fs::write(retired.join("leftover"), b"x").expect("leftover");
let reservation: PathBuf = scene.registry.join(RESERVATION_LOCK_NAME);
std::fs::create_dir(&reservation).expect("reservation");
std::fs::write(
reservation.join("owner.json"),
encode_owner_lock_record(&OwnerLockRecord {
token: String::from("t"),
owner_pid: i64::from(std::process::id()),
owner_birth_identity: String::from("linux:1"),
transaction_id: None,
}),
)
.expect("dead reservation");
let outcomes: Vec<RecoveryOutcome> =
recover_registered_transactions(&scene.place(), scene.registry.as_path())
.expect("recovered");
assert_eq!(outcomes.len(), 5);
assert_eq!(outcomes[0].directory, scene.directory);
assert_eq!(outcomes[0].action, RecoveryAction::CommitNotCreated);
assert_eq!(
action_for(outcomes.as_slice(), live.as_path()),
Some(RecoveryAction::OwnerActive)
);
assert_eq!(
action_for(outcomes.as_slice(), retained.as_path()),
Some(RecoveryAction::StagingRetained)
);
assert_eq!(
action_for(outcomes.as_slice(), unattributed.as_path()),
Some(RecoveryAction::StagingUnattributed)
);
assert_eq!(
action_for(outcomes.as_slice(), retired.as_path()),
Some(RecoveryAction::RetiredRemoved)
);
assert_eq!(outcomes[4].action, RecoveryAction::RetiredRemoved);
assert!(!scene.directory.exists());
assert!(live.exists());
assert!(retained.exists());
assert!(unattributed.exists());
assert!(!retired.exists());
assert!(!reservation.exists());
assert!(!scene.registry.join(LANDING_LOCK_NAME).exists());
scene.finish();
}
#[test]
fn landing_pass_entries_are_recovered_once() {
let scene: Scene = Scene::new("recovery-scan-once", false);
scene.capture();
scene.land(LandingOperation::Commit, Vec::new());
let second: PathBuf = add_entry(
scene.registry.as_path(),
SECOND,
Some(owner_bytes(SECOND, "2026-10-06T00:00:01.000Z", false)),
);
std::fs::write(
second.join("index-lock-1.json"),
std::fs::read(scene.directory.join("index-lock-1.json")).expect("record"),
)
.expect("second lock record");
let outcomes: Vec<RecoveryOutcome> =
recover_registered_transactions(&scene.place(), scene.registry.as_path())
.expect("recovered");
assert_eq!(outcomes.len(), 2);
assert_eq!(
action_for(outcomes.as_slice(), second.as_path()),
Some(RecoveryAction::CommitNotCreated)
);
assert!(!second.exists());
assert!(recovered_already(outcomes.as_slice(), second.as_path()));
assert!(!recovered_already(
outcomes.as_slice(),
scene.root.as_path()
));
scene.finish();
}
#[test]
fn transactions_without_landings_need_no_landing_lock() {
let scene: Scene = Scene::new("recovery-scan-no-landing", false);
scene.capture();
let held: crate::owner_lock::OwnerLock =
try_acquire_owner_lock(scene.registry.join(LANDING_LOCK_NAME).as_path(), None)
.expect("acquire")
.expect("free");
let outcomes: Vec<RecoveryOutcome> =
recover_registered_transactions(&scene.place(), scene.registry.as_path())
.expect("recovered");
assert_eq!(outcomes.len(), 1);
assert_eq!(outcomes[0].action, RecoveryAction::CommitNotCreated);
held.release().expect("release");
scene.finish();
}
#[test]
fn a_dead_transaction_holding_only_its_index_lock_waits_for_the_landing_lock() {
let scene: Scene = Scene::new("recovery-scan-index-lock-only", false);
scene.capture();
scene.land(LandingOperation::Commit, Vec::new());
for entry in std::fs::read_dir(scene.directory.as_path()).expect("listing") {
let name: String = entry.expect("entry").file_name().to_string_lossy().into_owned();
if name.starts_with("landing-") {
std::fs::remove_file(scene.directory.join(name)).expect("drop the landing record");
}
}
assert!(scene.directory.join("index-lock-1.json").exists());
let landing_lock: PathBuf = scene.registry.join(LANDING_LOCK_NAME);
let (ready_sender, ready_receiver) = std::sync::mpsc::channel::<()>();
let lander = std::thread::spawn(move || {
let held: crate::owner_lock::OwnerLock = try_acquire_owner_lock(landing_lock.as_path(), None)
.expect("acquire")
.expect("free");
ready_sender.send(()).expect("ready");
std::thread::sleep(std::time::Duration::from_millis(600));
held.release().expect("release");
});
ready_receiver.recv().expect("the lander holds the lock");
let started: std::time::Instant = std::time::Instant::now();
let outcomes: Vec<RecoveryOutcome> =
recover_registered_transactions(&scene.place(), scene.registry.as_path())
.expect("recovered");
assert!(
started.elapsed() >= std::time::Duration::from_millis(400),
"recovery of a transaction that holds a real index lock must wait for the landing lock"
);
lander.join().expect("lander");
assert_eq!(outcomes.len(), 1);
assert_eq!(outcomes[0].action, RecoveryAction::CommitNotCreated);
scene.finish();
}
#[test]
fn a_transaction_a_peer_removed_after_inspection_needs_no_landing_lock() {
let scene: Scene = Scene::new("recovery-scan-vanished", false);
scene.capture();
scene.land(LandingOperation::Commit, Vec::new());
let entries: Vec<RegistryEntry> = list_transaction_entries(scene.registry.as_path()).expect("listing");
let inspected: InspectedEntry = inspect_registry_entry(&entries[0]).expect("inspected");
assert!(needs_landing_lock(&inspected).expect("present"));
std::fs::remove_dir_all(scene.directory.as_path()).expect("the peer removes the transaction");
assert!(!needs_landing_lock(&inspected).expect("a vanished transaction needs no lock"));
assert!(!entered_landing_unless_vanished(scene.directory.as_path()).expect("vanished"));
let file: PathBuf = scene.root.join("not-a-directory");
std::fs::write(file.as_path(), b"x").expect("file");
assert!(entered_landing_unless_vanished(file.as_path()).is_err());
scene.finish();
}
#[test]
fn a_dead_transaction_a_peer_finished_after_inspection_is_vanished() {
let scene: Scene = Scene::new("recovery-scan-finished-by-peer", false);
scene.capture();
let entries: Vec<RegistryEntry> = list_transaction_entries(scene.registry.as_path()).expect("listing");
let inspected: InspectedEntry = inspect_registry_entry(&entries[0]).expect("inspected");
assert!(inspected.dead_owner().is_some());
std::fs::remove_dir_all(scene.directory.as_path()).expect("the peer finishes the transaction");
assert_eq!(
recover_inspected_entry(&scene.place(), &inspected).expect("a finished transaction is not a failure"),
RecoveryAction::Vanished
);
scene.finish();
}
#[test]
fn failures_keep_their_directory() {
let scene: Scene = Scene::new("recovery-scan-failure", false);
std::fs::write(scene.directory.join("journal.json"), b"{}").expect("unreleased");
assert!(recover_registered_transactions(&scene.place(), scene.registry.as_path()).is_err());
assert!(scene.directory.exists());
std::fs::remove_file(scene.directory.join("journal.json")).expect("remove");
std::fs::write(scene.directory.join("landing-1.json"), b"{}").expect("malformed landing");
std::fs::write(scene.directory.join("preparing.json"), b"{}").expect("malformed preparing");
assert!(recover_registered_transactions(&scene.place(), scene.registry.as_path()).is_err());
assert!(!scene.registry.join(LANDING_LOCK_NAME).exists());
assert!(scene.directory.exists());
std::fs::write(scene.registry.join("stray"), b"").expect("stray");
assert!(recover_registered_transactions(&scene.place(), scene.registry.as_path()).is_err());
std::fs::remove_file(scene.registry.join("stray")).expect("remove stray");
std::fs::remove_dir_all(&scene.directory).expect("remove transaction");
let reservation: crate::owner_lock::OwnerLock =
try_acquire_owner_lock(scene.registry.join(RESERVATION_LOCK_NAME).as_path(), None)
.expect("acquire")
.expect("free");
assert_eq!(
recover_registered_transactions(&scene.place(), scene.registry.as_path()).expect("empty"),
Vec::new()
);
assert!(scene.registry.join(RESERVATION_LOCK_NAME).exists());
reservation.release().expect("release");
scene.finish();
}
#[test]
fn registry_locks_are_owner_locks() {
let scene: Scene = Scene::new("recovery-scan-lock", true);
let lock: crate::owner_lock::OwnerLock =
acquire_registry_lock(scene.registry.as_path(), LANDING_LOCK_NAME).expect("acquire");
assert!(
scene
.registry
.join(LANDING_LOCK_NAME)
.join("owner.json")
.exists()
);
lock.release().expect("release");
std::fs::write(scene.registry.join("file.lock"), b"").expect("file in the way");
assert!(acquire_registry_lock(scene.registry.join("file.lock").as_path(), "x").is_err());
scene.finish();
}
#[test]
fn the_main_pass_leaves_the_landing_pass_its_entries() {
let recovered: PathBuf = PathBuf::from("/registry/recovered");
let other: PathBuf = PathBuf::from("/registry/other");
let outcomes: Vec<RecoveryOutcome> = vec![RecoveryOutcome {
directory: recovered.clone(),
action: RecoveryAction::RetiredRemoved,
}];
assert!(handled_by_the_landing_pass(true, outcomes.as_slice(), other.as_path()));
assert!(handled_by_the_landing_pass(false, outcomes.as_slice(), recovered.as_path()));
assert!(handled_by_the_landing_pass(true, outcomes.as_slice(), recovered.as_path()));
assert!(!handled_by_the_landing_pass(false, outcomes.as_slice(), other.as_path()));
}