use std::path::PathBuf;
use std::process::{Child, Command};
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
use torrust_tracker_deployer_lib::infrastructure::persistence::filesystem::file_lock::{
FileLock, FileLockError,
};
fn spawn_lock_holder(lock_file: &std::path::Path, duration_secs: u64) -> Child {
Command::new(env!("CARGO_BIN_EXE_lock_holder_helper"))
.arg(lock_file.to_str().expect("Invalid path"))
.arg(duration_secs.to_string())
.spawn()
.expect("Failed to spawn child process")
}
#[test]
fn it_should_prevent_lock_acquisition_across_processes() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lock_file = temp_dir.path().join("cross_process.json");
let mut child = spawn_lock_holder(&lock_file, 2);
thread::sleep(Duration::from_millis(200));
let result = FileLock::acquire(&lock_file, Duration::from_millis(500));
assert!(
matches!(result, Err(FileLockError::AcquisitionTimeout { .. })),
"Should timeout when child process holds lock"
);
let exit_status = child.wait().expect("Failed to wait for child");
assert!(
exit_status.success(),
"Child process should exit successfully"
);
}
#[test]
fn it_should_acquire_lock_after_child_releases() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lock_file = temp_dir.path().join("handoff.json");
let mut child = spawn_lock_holder(&lock_file, 1);
thread::sleep(Duration::from_millis(200));
let result = FileLock::acquire(&lock_file, Duration::from_secs(3));
assert!(
result.is_ok(),
"Should eventually acquire after child releases lock: {:?}",
result.err()
);
let exit_status = child.wait().expect("Failed to wait for child");
assert!(
exit_status.success(),
"Child process should exit successfully"
);
}
#[test]
#[cfg(unix)] fn it_should_clean_up_stale_lock_after_process_crash() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lock_file = temp_dir.path().join("crash.json");
let mut child = spawn_lock_holder(&lock_file, 10);
thread::sleep(Duration::from_millis(200));
child.kill().expect("Failed to kill child process");
child.wait().expect("Failed to wait for child");
thread::sleep(Duration::from_millis(100));
let result = FileLock::acquire(&lock_file, Duration::from_secs(2));
assert!(
result.is_ok(),
"Should clean up stale lock from crashed process: {:?}",
result.err()
);
}
#[test]
fn it_should_handle_rapid_lock_handoff_between_processes() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lock_file = temp_dir.path().join("rapid_handoff.json");
let mut children = vec![];
for i in 0..5 {
let child = spawn_lock_holder(&lock_file, 1);
thread::sleep(Duration::from_millis(100));
children.push((i, child));
}
for (i, mut child) in children {
let exit_status = child.wait().expect("Failed to wait for child");
assert!(exit_status.success(), "Child {i} should exit successfully");
}
let result = FileLock::acquire(&lock_file, Duration::from_secs(1));
assert!(
result.is_ok(),
"Should acquire lock after rapid handoffs: {:?}",
result.err()
);
}
#[test]
fn it_should_handle_multiple_processes_competing_for_lock() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lock_file = temp_dir.path().join("competition.json");
let mut children = vec![];
for i in 0..3 {
let child = spawn_lock_holder(&lock_file, 2);
children.push((i, child));
thread::sleep(Duration::from_millis(10));
}
let mut successful = 0;
let mut failed = 0;
for (i, mut child) in children {
let exit_status = child.wait().expect("Failed to wait for child");
if exit_status.success() {
successful += 1;
} else {
failed += 1;
}
println!("Child {i} exit status: {exit_status:?}");
}
assert!(
successful >= 1,
"At least one process should successfully acquire the lock"
);
println!("Competition test: {successful} successful, {failed} failed");
}
#[test]
fn it_should_allow_sequential_acquisition_by_different_processes() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lock_file = temp_dir.path().join("sequential.json");
for i in 0..3 {
let mut child = spawn_lock_holder(&lock_file, 1);
let exit_status = child.wait().expect("Failed to wait for child");
assert!(
exit_status.success(),
"Sequential acquisition {i} should succeed"
);
}
}
#[test]
#[cfg(unix)] fn it_should_detect_stale_locks_with_dead_process_ids() {
use std::fs;
use torrust_tracker_deployer_lib::infrastructure::persistence::filesystem::process_id::ProcessId;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lock_file = temp_dir.path().join("stale_detection.json");
let lock_file_path = FileLock::lock_file_path(&lock_file);
let fake_pid = 999_999;
fs::write(&lock_file_path, fake_pid.to_string()).expect("Failed to create stale lock file");
let pid = ProcessId::from_raw(fake_pid);
assert!(!pid.is_alive(), "Fake PID should not be alive");
let result = FileLock::acquire(&lock_file, Duration::from_secs(2));
assert!(
result.is_ok(),
"Should clean up stale lock and acquire: {:?}",
result.err()
);
}
#[test]
fn it_should_handle_parent_acquiring_while_child_holds_lock() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lock_file = temp_dir.path().join("parent_child.json");
let mut child = spawn_lock_holder(&lock_file, 3);
thread::sleep(Duration::from_millis(200));
let parent_result = FileLock::acquire(&lock_file, Duration::from_millis(500));
assert!(
matches!(parent_result, Err(FileLockError::AcquisitionTimeout { .. })),
"Parent should timeout while child holds lock"
);
let exit_status = child.wait().expect("Failed to wait for child");
assert!(exit_status.success(), "Child should exit successfully");
let parent_retry = FileLock::acquire(&lock_file, Duration::from_secs(1));
assert!(
parent_retry.is_ok(),
"Parent should acquire after child releases"
);
}
trait FileLockPathExt {
fn lock_file_path(file_path: &std::path::Path) -> PathBuf;
}
impl FileLockPathExt for FileLock {
fn lock_file_path(file_path: &std::path::Path) -> PathBuf {
let mut lock_path = file_path.to_path_buf();
let current_extension = lock_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let new_extension = if current_extension.is_empty() {
"lock".to_string()
} else {
format!("{current_extension}.lock")
};
lock_path.set_extension(new_extension);
lock_path
}
}