use super::names::{
Outcome, PHASES_AFTER_THE_BRANCH_MOVED, PHASES_BEFORE_THE_LANDING_BARRIER, lose_name, settle, transaction_directory,
};
use super::{Paused, Probe, pause_at, probe};
use crate::commit::remove_settled;
use crate::support::Sleeper;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
pub(super) fn age_tree(directory: &Path) {
let long_ago: SystemTime = SystemTime::now() - Duration::from_secs(2 * 3600);
let mut pending: Vec<PathBuf> = vec![directory.to_path_buf()];
let mut ordered: Vec<PathBuf> = Vec::new();
while let Some(path) = pending.pop() {
let metadata = std::fs::symlink_metadata(&path).expect("entry to age");
assert!(!metadata.file_type().is_symlink(), "a link keeps its date: {}", path.display());
if metadata.is_dir() {
for entry in std::fs::read_dir(&path).expect("directory to age").flatten() {
pending.push(entry.path());
}
}
ordered.push(path);
}
for path in ordered.iter().rev() {
std::fs::File::open(path).expect("open").set_modified(long_ago).expect("age");
}
}
pub(super) fn status(paused: &Paused) -> Probe {
return probe(&paused.fixture, paused.repo.as_path(), &["status", "--porcelain=v1"]);
}
#[test]
fn a_lost_owner_name_stops_a_fresh_directory_and_names_the_way_out() {
for phase in PHASES_BEFORE_THE_LANDING_BARRIER {
let paused: Paused = pause_at(phase);
let directory: PathBuf = transaction_directory(paused.repo.join(".git").as_path());
lose_name(directory.as_path(), "owner.json");
let first: Probe = status(&paused);
let second: Probe = status(&paused);
let context: String = format!("{phase}: {}", first.stderr);
assert_eq!(first.code, Some(2), "{context}");
assert_eq!(second.code, Some(2), "{context}");
assert!(first.stderr.contains("Transaction owner record is missing"), "{context}");
assert!(first.stderr.contains(directory.to_string_lossy().as_ref()), "the directory is named: {context}");
assert!(first.stderr.contains(format!("rm -r {}", directory.display()).as_str()), "the removal is exact: {context}");
assert!(first.stderr.contains("Its newest entry changed"), "why it is not recovered: {context}");
assert!(first.stderr.contains("60 minutes"), "when the wrapper acts by itself: {context}");
assert!(first.stderr.contains("next guarded command"), "which command recovers it: {context}");
assert!(first.stderr.contains("landing-<n>.json"), "when removal by hand loses work: {context}");
assert!(directory.exists(), "{context}");
remove_settled(&paused.fixture);
}
}
#[test]
fn a_lost_owner_name_in_an_aged_directory_converges() {
for phase in PHASES_BEFORE_THE_LANDING_BARRIER.iter().chain(PHASES_AFTER_THE_BRANCH_MOVED.iter()) {
let paused: Paused = pause_at(phase);
let directory: PathBuf = transaction_directory(paused.repo.join(".git").as_path());
lose_name(directory.as_path(), "owner.json");
age_tree(directory.as_path());
let outcome: Outcome = settle(paused);
let expected: &str = if PHASES_AFTER_THE_BRANCH_MOVED.contains(phase) { "completes" } else { "discards" };
assert_eq!(outcome.cell, expected, "{phase}: {outcome:?}");
}
}
#[test]
fn a_live_process_in_an_aged_directory_keeps_the_stop_until_it_is_gone() {
for phase in ["capture-locked", "landing-locked"] {
let paused: Paused = pause_at(phase);
let directory: PathBuf = transaction_directory(paused.repo.join(".git").as_path());
lose_name(directory.as_path(), "owner.json");
age_tree(directory.as_path());
let holder: Sleeper = Sleeper::start(directory.as_path(), None);
let blocked: Probe = status(&paused);
let context: String = format!("{phase}: {}", blocked.stderr);
assert_eq!(blocked.code, Some(2), "{context}");
assert!(blocked.stderr.contains(format!("PID {} (sleep)", holder.pid()).as_str()), "the process is named: {context}");
assert!(directory.exists(), "{context}");
drop(holder);
let outcome: Outcome = settle(paused);
assert_eq!(outcome.cell, "discards", "{phase}: {outcome:?}");
}
}