use super::*;
use crate::test_support::{fixture, remove};
use crate::transaction_owner::{current_transaction_owner, encode_transaction_owner};
use crate::transaction_registry::{OWNER_FILENAME, TRANSACTION_ROOT_NAME, ensure_transaction_root};
const FIRST: &str = "0b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10";
const SECOND: &str = "1c7d3d2f-7a6c-4e1f-8b66-4a6e9f3f7b21";
fn transaction(registry: &Path, id: &str, created_at: &str, live: bool) -> PathBuf {
let directory: PathBuf = registry.join(id);
std::fs::create_dir_all(&directory).expect("transaction directory");
let mut owner = current_transaction_owner(id, created_at).expect("current owner");
if !live {
owner.owner_identity = String::from("linux:1");
}
std::fs::write(directory.join(OWNER_FILENAME), encode_transaction_owner(&owner)).expect("owner record");
return directory;
}
fn registry_of(name: &str) -> (PathBuf, PathBuf) {
let root: PathBuf = fixture(name);
let registry: PathBuf = root.join(TRANSACTION_ROOT_NAME);
ensure_transaction_root(registry.as_path()).expect("registry root");
return (root, registry);
}
#[test]
fn request_bytes_match_the_incumbent_object() {
assert_eq!(
reservation_request_bytes(2),
b"{\"schemaVersion\":2,\"state\":\"reservation-request\",\"lostRaces\":2}\n".to_vec()
);
}
#[test]
fn the_reservation_is_earned_at_the_configured_count() {
assert!(!reservation_earned(0, 1));
assert!(reservation_earned(1, 1));
assert!(!reservation_earned(2, 3));
assert!(reservation_earned(3, 3));
assert!(reservation_earned(4, 3));
}
#[test]
fn requesters_are_live_transactions_with_a_request() {
let (root, registry) = registry_of("replay-reservation-requesters");
assert!(live_requesters(registry.as_path()).expect("empty").is_empty());
let live: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
assert!(live_requesters(registry.as_path()).expect("no request").is_empty());
std::fs::write(live.join(RESERVATION_REQUEST_FILENAME), reservation_request_bytes(1)).expect("request");
assert_eq!(live_requesters(registry.as_path()).expect("requester"), vec![String::from(FIRST)]);
let dead: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-09T00:00:00.000Z", false);
std::fs::write(dead.join(RESERVATION_REQUEST_FILENAME), reservation_request_bytes(1)).expect("request");
assert_eq!(live_requesters(registry.as_path()).expect("dead excluded"), vec![String::from(FIRST)]);
remove(root.as_path());
}
#[test]
fn the_oldest_requester_is_granted_first() {
let (root, registry) = registry_of("replay-reservation-order");
for (id, created) in [(FIRST, "2026-10-10T00:00:05.000Z"), (SECOND, "2026-10-10T00:00:01.000Z")] {
let directory: PathBuf = transaction(registry.as_path(), id, created, true);
std::fs::write(directory.join(RESERVATION_REQUEST_FILENAME), reservation_request_bytes(1)).expect("request");
}
assert_eq!(
live_requesters(registry.as_path()).expect("order"),
vec![String::from(SECOND), String::from(FIRST)]
);
remove(root.as_path());
}
#[test]
fn a_held_reservation_is_seen_by_others_until_released() {
let (root, registry) = registry_of("replay-reservation-holder");
let first_directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
let second_directory: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
let mut first: LandingReservation = LandingReservation::open(registry.as_path(), first_directory.as_path(), FIRST, false);
let second: LandingReservation = LandingReservation::open(registry.as_path(), second_directory.as_path(), SECOND, false);
assert!(first.reserve(1).expect("granted"));
assert!(first_directory.join(RESERVATION_REQUEST_FILENAME).exists());
assert!(!first.held_by_another().expect("own view"));
assert!(second.held_by_another().expect("other view"));
first.release().expect("released");
assert!(!first_directory.join(RESERVATION_REQUEST_FILENAME).exists());
assert!(!second.held_by_another().expect("free"));
remove(root.as_path());
}
#[test]
fn a_nested_landing_neither_requests_nor_waits() {
let (root, registry) = registry_of("replay-reservation-nested");
let directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
let mut nested: LandingReservation = LandingReservation::open(registry.as_path(), directory.as_path(), FIRST, true);
assert!(!nested.reserve(1).expect("not applicable"));
assert!(!directory.join(RESERVATION_REQUEST_FILENAME).exists());
assert!(!nested.held_by_another().expect("nested never waits"));
nested.await_slot().expect("returns at once");
remove(root.as_path());
}
#[test]
fn the_grant_follows_the_configured_lost_races() {
let (root, registry) = registry_of("replay-reservation-threshold");
let directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
let configured: u64 = 3;
let mut reservation: LandingReservation = LandingReservation::open(registry.as_path(), directory.as_path(), FIRST, false);
let mut granted_at: Option<u64> = None;
for lost in 1..=configured {
if reservation_earned(lost, configured) && reservation.reserve(lost).expect("reserve") {
granted_at = Some(lost);
break;
}
}
assert_eq!(granted_at, Some(configured));
let request: Vec<u8> = std::fs::read(directory.join(RESERVATION_REQUEST_FILENAME)).expect("request");
assert_eq!(request, reservation_request_bytes(configured));
reservation.release().expect("released");
remove(root.as_path());
}
#[test]
fn a_landing_takes_the_landing_lock_once_the_reservation_is_free() {
let (root, registry) = registry_of("replay-reservation-yield");
let holder_directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
let mut holder: LandingReservation = LandingReservation::open(registry.as_path(), holder_directory.as_path(), FIRST, false);
assert!(holder.reserve(1).expect("granted"));
holder.release().expect("released");
let waiter_directory: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
let waiter: LandingReservation = LandingReservation::open(registry.as_path(), waiter_directory.as_path(), SECOND, false);
let landing_lock: PathBuf = registry.join("landing.lock");
let lock: OwnerLock = acquire_reserved_landing_lock(&waiter, || {
let acquired = try_acquire_owner_lock(landing_lock.as_path(), Some(SECOND))
.map_err(ReservationError::Lock)?;
return acquired.ok_or_else(|| { return ReservationError::Registry(RecoveryError(String::from("landing lock busy"))); });
})
.expect("landing lock after the reservation is free");
lock.release().expect("released landing lock");
remove(root.as_path());
}
fn finishes_in_time<Output: Send + 'static>(work: impl FnOnce() -> Output + Send + 'static) -> Output {
let (sender, receiver) = std::sync::mpsc::channel::<Output>();
std::thread::spawn(move || {
let _sent = sender.send(work());
});
return receiver
.recv_timeout(Duration::from_secs(10))
.expect("the wait did not end at its limit");
}
#[test]
fn a_reservation_held_past_the_limit_ends_the_wait() {
let (root, registry) = registry_of("replay-reservation-await-timeout");
let first_directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
let second_directory: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
let mut holder: LandingReservation =
LandingReservation::open(registry.as_path(), first_directory.as_path(), FIRST, false);
assert!(holder.reserve(1).expect("granted"));
let waiter: LandingReservation =
LandingReservation::open(registry.as_path(), second_directory.as_path(), SECOND, false);
let started: Instant = Instant::now();
let failure: ReservationError = finishes_in_time(move || {
return waiter.await_slot_for(Duration::from_millis(200)).expect_err("held past the limit");
});
assert!(started.elapsed() >= Duration::from_millis(200), "{:?}", started.elapsed());
match failure {
ReservationError::WaitTimedOut { waited, holder_pid } => {
assert!(waited >= Duration::from_millis(200), "{waited:?}");
assert_eq!(holder_pid, Some(i64::from(std::process::id())));
}
other => panic!("expected a refused wait, got {other}"),
}
assert!(holder.held_by_another().is_ok());
holder.release().expect("released");
remove(root.as_path());
}
#[test]
fn a_wait_is_over_at_its_limit_exactly() {
let limit: Duration = Duration::from_millis(200);
assert!(!limit_reached(Duration::from_millis(199), limit));
assert!(limit_reached(limit, limit));
assert!(limit_reached(Duration::from_millis(201), limit));
}
#[test]
fn the_default_wait_lasts_as_long_as_the_reservation() {
let (root, registry) = registry_of("replay-reservation-await-default");
let first_directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
let second_directory: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
let mut holder: LandingReservation =
LandingReservation::open(registry.as_path(), first_directory.as_path(), FIRST, false);
assert!(holder.reserve(1).expect("granted"));
let waiter: LandingReservation =
LandingReservation::open(registry.as_path(), second_directory.as_path(), SECOND, false);
let (sender, receiver) = std::sync::mpsc::channel::<Result<(), ReservationError>>();
std::thread::spawn(move || {
let _sent = sender.send(waiter.await_slot());
});
assert!(
receiver.recv_timeout(Duration::from_millis(400)).is_err(),
"the wait ended while the reservation stood"
);
holder.release().expect("released");
receiver
.recv_timeout(Duration::from_secs(10))
.expect("the wait did not end after the release")
.expect("the wait succeeds");
remove(root.as_path());
}
#[cfg(unix)]
#[test]
fn a_release_without_a_request_succeeds_and_other_failures_are_reported() {
let (root, registry) = registry_of("replay-reservation-release-kinds");
let directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
LandingReservation::open(registry.as_path(), directory.as_path(), FIRST, false)
.release()
.expect("no request to remove");
let file: PathBuf = root.join("not-a-directory");
std::fs::write(&file, b"x").expect("file");
let failure: ReservationError = LandingReservation::open(registry.as_path(), file.as_path(), FIRST, false)
.release()
.expect_err("the request path cannot be inspected");
assert!(matches!(failure, ReservationError::Write(_)), "{failure}");
remove(root.as_path());
}
#[test]
fn an_older_request_held_past_the_limit_ends_the_wait_for_the_grant() {
let (root, registry) = registry_of("replay-reservation-reserve-timeout");
let older: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
std::fs::write(older.join(RESERVATION_REQUEST_FILENAME), reservation_request_bytes(1)).expect("older request");
let younger: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
let registry_for_thread: PathBuf = registry.clone();
let younger_for_thread: PathBuf = younger.clone();
let (failure, reservation) = finishes_in_time(move || {
let mut reservation: LandingReservation = LandingReservation::open(
registry_for_thread.as_path(),
younger_for_thread.as_path(),
SECOND,
false,
);
let failure: ReservationError = reservation
.reserve_for(2, Duration::from_millis(200))
.expect_err("never first in line");
return (failure, reservation);
});
match failure {
ReservationError::WaitTimedOut { waited, holder_pid } => {
assert!(waited >= Duration::from_millis(200), "{waited:?}");
assert_eq!(holder_pid, None);
}
other => panic!("expected a refused wait, got {other}"),
}
assert!(younger.join(RESERVATION_REQUEST_FILENAME).exists());
reservation.release().expect("released");
assert!(!younger.join(RESERVATION_REQUEST_FILENAME).exists());
assert!(older.join(RESERVATION_REQUEST_FILENAME).exists(), "the older request is untouched");
remove(root.as_path());
}
#[test]
fn a_refused_wait_is_described_and_other_failures_keep_their_debug_text() {
assert_eq!(
ReservationError::WaitTimedOut { waited: Duration::from_millis(61_900), holder_pid: Some(77) }.to_string(),
"gave up after waiting 61 seconds for the next landing slot because process 77 holds the reservation; \
CLI_GIT_LOCK_WAIT_SECONDS sets how long to wait, and nothing was taken from that process"
);
assert!(
ReservationError::WaitTimedOut { waited: Duration::from_secs(2), holder_pid: None }
.to_string()
.contains("because an older live landing request holds the next slot")
);
let other: ReservationError = ReservationError::Identity(String::from("probe failed"));
assert_eq!(other.to_string(), format!("{other:?}"));
}