use super::*;
use crate::process_identity::current_birth_identity;
use std::ffi::OsStr;
use crate::test_support::{fixture, remove};
const PREFIX_TEXT: &str = "item.owner";
fn prefix() -> &'static OsStr {
return OsStr::new(PREFIX_TEXT);
}
const FIRST: u64 = 1;
const NO_SUCH_BIRTH: &str = "linux:not-a-birth";
fn held(outcome: Result<ClaimOutcome, MigrateError>) -> OwnerClaim {
return match outcome.expect("no failure") {
ClaimOutcome::Held(claim) => claim,
ClaimOutcome::Busy(owner) => panic!("expected a held claim, found the owner {owner:?}"),
};
}
fn busy(outcome: Result<ClaimOutcome, MigrateError>) -> BusyOwner {
return match outcome.expect("no failure") {
ClaimOutcome::Busy(owner) => owner,
ClaimOutcome::Held(claim) => panic!("expected a live owner, took {claim:?}"),
};
}
fn present(directory: &Path) -> Vec<u64> {
return generations(directory, prefix()).expect("listing");
}
#[test]
fn the_first_claim_is_generation_one() {
let directory: PathBuf = fixture("claim-first");
let claim: OwnerClaim = held(take_claim(directory.as_path(), prefix()));
assert_eq!(present(directory.as_path()), vec![FIRST]);
let text: String = std::fs::read_to_string(&claim.path).expect("claim file");
assert!(text.contains(format!("pid={}\n", std::process::id()).as_str()), "{text}");
remove(directory.as_path());
}
#[test]
fn a_live_owner_stops_the_next_claimant() {
let directory: PathBuf = fixture("claim-live");
let first: OwnerClaim = held(take_claim(directory.as_path(), prefix()));
let owner: BusyOwner = busy(take_claim(directory.as_path(), prefix()));
assert_eq!(owner, BusyOwner { pid: i64::from(std::process::id()), path: first.path.clone() });
assert_eq!(present(directory.as_path()), vec![FIRST], "the refused claimant created nothing");
remove(directory.as_path());
}
#[test]
fn a_released_claim_is_replaced_by_the_next_generation() {
let directory: PathBuf = fixture("claim-released");
let mut first: OwnerClaim = held(take_claim(directory.as_path(), prefix()));
first.release().expect("released");
let second: OwnerClaim = held(take_claim(directory.as_path(), prefix()));
assert_eq!(present(directory.as_path()), vec![FIRST + 1], "the older generation is gone");
drop(second);
let third: OwnerClaim = held(take_claim(directory.as_path(), prefix()));
assert_eq!(present(directory.as_path()), vec![FIRST + 2]);
drop(third);
assert_eq!(present(directory.as_path()), vec![FIRST + 2], "the newest generation is never removed, even when released");
remove(directory.as_path());
}
#[test]
fn an_owner_that_died_is_replaced() {
let directory: PathBuf = fixture("claim-died");
plant_claim_for_tests(directory.as_path(), prefix(), FIRST, unused_pid_for_tests(), NO_SUCH_BIRTH);
let claim: OwnerClaim = held(take_claim(directory.as_path(), prefix()));
assert_eq!(present(directory.as_path()), vec![FIRST + 1]);
drop(claim);
remove(directory.as_path());
}
#[test]
fn an_owner_whose_process_id_was_reused_is_replaced() {
let directory: PathBuf = fixture("claim-reused");
plant_claim_for_tests(directory.as_path(), prefix(), FIRST, i64::from(std::process::id()), NO_SUCH_BIRTH);
let claim: OwnerClaim = held(take_claim(directory.as_path(), prefix()));
assert_eq!(present(directory.as_path()), vec![FIRST + 1]);
drop(claim);
remove(directory.as_path());
}
#[test]
fn only_one_of_many_simultaneous_claimants_holds_the_item() {
let directory: PathBuf = fixture("claim-simultaneous");
plant_claim_for_tests(directory.as_path(), prefix(), FIRST, unused_pid_for_tests(), NO_SUCH_BIRTH);
let claimants: usize = std::thread::available_parallelism().map_or(2, std::num::NonZero::get);
let barrier: std::sync::Arc<std::sync::Barrier> = std::sync::Arc::new(std::sync::Barrier::new(claimants));
let handles: Vec<std::thread::JoinHandle<Option<OwnerClaim>>> = (0..claimants)
.map(|_index: usize| {
let (claimant_directory, claimant_barrier) = (directory.clone(), std::sync::Arc::clone(&barrier));
return std::thread::spawn(move || {
claimant_barrier.wait();
return match take_claim(claimant_directory.as_path(), prefix()).expect("no failure") {
ClaimOutcome::Held(claim) => Some(claim),
ClaimOutcome::Busy(_owner) => None,
};
});
})
.collect();
let winners: Vec<OwnerClaim> = handles.into_iter().filter_map(|handle: std::thread::JoinHandle<Option<OwnerClaim>>| { return handle.join().expect("claimant"); }).collect();
assert_eq!(winners.len(), 1, "exactly one claimant holds the item");
remove(directory.as_path());
}
#[test]
fn only_this_items_numbered_files_are_generations() {
let directory: PathBuf = fixture("claim-names");
for name in ["item.owner", "item.owner.", "item.owner.x", "item.owner.1.x", "item.owner.-1", "other.owner.9", "xitem.owner.5"] {
std::fs::write(directory.join(name), b"unrelated").expect("file");
}
let claim: OwnerClaim = held(take_claim(directory.as_path(), prefix()));
assert_eq!(present(directory.as_path()), vec![FIRST]);
drop(claim);
remove(directory.as_path());
}
#[test]
fn a_file_that_is_not_a_claim_stops_the_claimant_untouched() {
let directory: PathBuf = fixture("claim-garbage");
std::fs::write(path_of(directory.as_path(), prefix(), FIRST), b"somebody else's text").expect("file");
let message: String = take_claim(directory.as_path(), prefix()).expect_err("cannot be judged").to_string();
assert!(message.contains("is not a claim file") && message.contains(format!("{}.{FIRST}", PREFIX_TEXT).as_str()), "{message}");
assert_eq!(std::fs::read(path_of(directory.as_path(), prefix(), FIRST)).expect("file"), b"somebody else's text");
assert_eq!(present(directory.as_path()), vec![FIRST]);
remove(directory.as_path());
}
#[test]
fn an_owner_in_another_pid_namespace_is_alive_while_its_claim_is_young() {
let directory: PathBuf = fixture("claim-foreign");
let record: Record = Record::Owned { pid: unused_pid_for_tests(), birth: String::from(NO_SUCH_BIRTH), namespace: Some(String::from("pid:[not-this-namespace]")) };
std::fs::write(path_of(directory.as_path(), prefix(), FIRST), encode(&record)).expect("planted claim");
let owner: BusyOwner = busy(take_claim(directory.as_path(), prefix()));
assert_eq!(owner.pid, unused_pid_for_tests());
remove(directory.as_path());
}
#[test]
fn the_text_of_a_claim_round_trips() {
for record in [
Record::Released,
Record::Owned { pid: i64::from(std::process::id()), birth: String::from("linux:42"), namespace: None },
Record::Owned { pid: i64::from(std::process::id()), birth: String::from("linux:43"), namespace: Some(String::from("pid:[1]")) },
] {
assert_eq!(decode(encode(&record).as_str()), Some(record));
}
for text in ["", "cli-git owner-claim v2\nreleased\n", "cli-git owner-claim v1\npid=0\nbirth=x\nnamespace=-\n", "cli-git owner-claim v1\npid=1\nbirth=\nnamespace=-\n"] {
assert_eq!(decode(text), None, "{text:?}");
}
}
#[test]
fn a_claimant_with_a_stale_listing_withdraws_when_a_newer_generation_exists() {
let directory: PathBuf = fixture("claim-stale-listing");
let newer: u64 = FIRST + 2;
let mut acted: bool = false;
let outcome = take_claim_seamed(directory.as_path(), prefix(), &mut || {
if !acted {
acted = true;
plant_claim_for_tests(directory.as_path(), prefix(), newer, i64::from(std::process::id()), current_birth_identity().expect("identity").as_str());
}
});
assert_eq!(busy(outcome).path, path_of(directory.as_path(), prefix(), newer));
assert_eq!(present(directory.as_path()), vec![newer], "the stale claimant left nothing of its own");
remove(directory.as_path());
}