#[allow(clippy::duplicate_mod)]
#[path = "test_support/mod.rs"]
mod test_support;
use anyhow::Result;
use serial_test::serial;
use std::sync::Arc;
use std::thread;
use tempfile::TempDir;
use test_support::EnvVarGuard;
use xchecker::lock::{FileLock, LockError};
fn setup_test_env() -> TempDir {
xchecker::paths::with_isolated_home()
}
#[test]
fn test_concurrent_lock_acquisition_same_process() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-concurrent-same-process";
let lock1 = FileLock::acquire(spec_id, false, None)?;
assert_eq!(lock1.spec_id(), spec_id);
assert!(FileLock::exists(spec_id));
let result = FileLock::acquire(spec_id, false, None);
assert!(result.is_err(), "Second lock acquisition should fail");
match result.unwrap_err() {
LockError::ConcurrentExecution {
spec_id: locked_spec,
pid,
..
} => {
assert_eq!(locked_spec, spec_id);
assert_eq!(pid, std::process::id());
}
other => panic!("Expected ConcurrentExecution error, got: {:?}", other),
}
lock1.release()?;
assert!(!FileLock::exists(spec_id));
let lock2 = FileLock::acquire(spec_id, false, None)?;
assert_eq!(lock2.spec_id(), spec_id);
Ok(())
}
#[test]
fn test_lock_held_by_active_process_exit_9() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-lock-exit-9";
let _lock = FileLock::acquire(spec_id, false, None)?;
let result = FileLock::acquire(spec_id, false, None);
assert!(result.is_err());
match result.unwrap_err() {
LockError::ConcurrentExecution { .. } => {
}
other => panic!("Expected ConcurrentExecution error, got: {:?}", other),
}
Ok(())
}
#[test]
fn test_lock_released_on_normal_exit() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-lock-normal-exit";
let lock = FileLock::acquire(spec_id, false, None)?;
assert!(FileLock::exists(spec_id));
lock.release()?;
assert!(
!FileLock::exists(spec_id),
"Lock should be removed after release"
);
let _lock2 = FileLock::acquire(spec_id, false, None)?;
Ok(())
}
#[test]
fn test_lock_cleanup_on_drop() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-lock-drop-cleanup";
{
let _lock = FileLock::acquire(spec_id, false, None)?;
assert!(FileLock::exists(spec_id));
}
assert!(!FileLock::exists(spec_id), "Lock should be removed by Drop");
let _lock2 = FileLock::acquire(spec_id, false, None)?;
Ok(())
}
#[test]
fn test_lock_cleanup_on_panic_simulation() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-lock-panic-cleanup";
let result = std::panic::catch_unwind(|| {
let _lock = FileLock::acquire(spec_id, false, None).unwrap();
assert!(FileLock::exists(spec_id));
});
assert!(result.is_ok());
assert!(
!FileLock::exists(spec_id),
"Lock should be cleaned up even after panic"
);
Ok(())
}
#[test]
fn test_lock_file_contains_correct_info() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-lock-info-content";
let _lock = FileLock::acquire(spec_id, false, None)?;
let lock_info = FileLock::get_lock_info(spec_id)?.expect("Lock info should exist");
assert_eq!(lock_info.spec_id, spec_id);
assert_eq!(lock_info.pid, std::process::id());
assert!(!lock_info.xchecker_version.is_empty());
assert!(lock_info.start_time > 0);
assert!(lock_info.created_at > 0);
Ok(())
}
#[test]
#[serial]
fn test_concurrent_threads_same_process() -> Result<()> {
let temp_dir = TempDir::new()?;
let _guard = EnvVarGuard::set("XCHECKER_HOME", temp_dir.path().to_str().unwrap());
let spec_id = "test-concurrent-threads";
let lock = FileLock::acquire(spec_id, false, None)?;
assert!(
FileLock::exists(spec_id),
"Lock should exist after acquisition"
);
let spec_id_arc = Arc::new(spec_id.to_string());
let mut handles = vec![];
for _ in 0..5 {
let spec_id = Arc::clone(&spec_id_arc);
let handle = thread::spawn(move || {
FileLock::acquire(&spec_id, false, None)
});
handles.push(handle);
}
let mut error_count = 0;
for handle in handles {
match handle.join().unwrap() {
Ok(_) => panic!("Thread should not have acquired lock while it's held"),
Err(LockError::ConcurrentExecution { .. }) => {
error_count += 1;
}
Err(other) => panic!("Expected ConcurrentExecution error, got: {:?}", other),
}
}
assert_eq!(
error_count, 5,
"All 5 threads should fail with ConcurrentExecution"
);
lock.release()?;
assert!(
!FileLock::exists(spec_id),
"Lock should not exist after release"
);
let lock2 = FileLock::acquire(spec_id, false, None)?;
assert!(
FileLock::exists(spec_id),
"Lock should be acquirable after release"
);
lock2.release()?;
Ok(())
}
#[test]
fn test_stale_lock_detection_and_force_override() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-stale-lock-force";
let spec_root = xchecker::paths::spec_root(spec_id);
let lock_path = spec_root.as_std_path().join(".lock");
std::fs::create_dir_all(lock_path.parent().unwrap())?;
let stale_lock_info = xchecker::lock::LockInfo {
pid: 99999, start_time: 0,
created_at: 0, spec_id: spec_id.to_string(),
xchecker_version: "0.1.0".to_string(),
};
let lock_json = serde_json::to_string_pretty(&stale_lock_info)?;
std::fs::write(&lock_path, lock_json)?;
let result = FileLock::acquire(spec_id, false, None);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), LockError::StaleLock { .. }));
let lock = FileLock::acquire(spec_id, true, None)?;
assert_eq!(lock.spec_id(), spec_id);
let new_lock_info = FileLock::get_lock_info(spec_id)?.unwrap();
assert_eq!(new_lock_info.pid, std::process::id());
Ok(())
}
#[test]
fn test_lock_with_dead_process_recent_timestamp() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-dead-process-recent";
let spec_root = xchecker::paths::spec_root(spec_id);
let lock_path = spec_root.as_std_path().join(".lock");
std::fs::create_dir_all(lock_path.parent().unwrap())?;
let recent_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
- 60;
let dead_process_lock = xchecker::lock::LockInfo {
pid: 99999, start_time: 0,
created_at: recent_time,
spec_id: spec_id.to_string(),
xchecker_version: "0.1.0".to_string(),
};
let lock_json = serde_json::to_string_pretty(&dead_process_lock)?;
std::fs::write(&lock_path, lock_json)?;
let result = FileLock::acquire(spec_id, false, None);
assert!(result.is_err());
let lock = FileLock::acquire(spec_id, true, None)?;
assert_eq!(lock.spec_id(), spec_id);
Ok(())
}
#[test]
fn test_configurable_ttl() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-configurable-ttl";
let spec_root = xchecker::paths::spec_root(spec_id);
let lock_path = spec_root.as_std_path().join(".lock");
std::fs::create_dir_all(lock_path.parent().unwrap())?;
let two_minutes_ago = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
- 120;
let old_lock = xchecker::lock::LockInfo {
pid: 99999,
start_time: 0,
created_at: two_minutes_ago,
spec_id: spec_id.to_string(),
xchecker_version: "0.1.0".to_string(),
};
let lock_json = serde_json::to_string_pretty(&old_lock)?;
std::fs::write(&lock_path, lock_json)?;
let result = FileLock::acquire(spec_id, false, Some(60));
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), LockError::StaleLock { .. }));
let result = FileLock::acquire(spec_id, false, Some(180));
assert!(result.is_err());
let lock = FileLock::acquire(spec_id, true, Some(60))?;
assert_eq!(lock.spec_id(), spec_id);
Ok(())
}
#[test]
fn test_multiple_specs_independent_locks() -> Result<()> {
let _temp_dir = setup_test_env();
let spec1 = "test-spec-1";
let spec2 = "test-spec-2";
let spec3 = "test-spec-3";
let lock1 = FileLock::acquire(spec1, false, None)?;
let lock2 = FileLock::acquire(spec2, false, None)?;
let lock3 = FileLock::acquire(spec3, false, None)?;
assert!(FileLock::exists(spec1));
assert!(FileLock::exists(spec2));
assert!(FileLock::exists(spec3));
assert_eq!(lock1.spec_id(), spec1);
assert_eq!(lock2.spec_id(), spec2);
assert_eq!(lock3.spec_id(), spec3);
lock1.release()?;
lock2.release()?;
lock3.release()?;
assert!(!FileLock::exists(spec1));
assert!(!FileLock::exists(spec2));
assert!(!FileLock::exists(spec3));
Ok(())
}
#[test]
fn test_lock_acquisition_creates_directory() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-new-spec-dir";
let spec_root = xchecker::paths::spec_root(spec_id);
let lock_path = spec_root.as_std_path().join(".lock");
assert!(!lock_path.exists());
let lock = FileLock::acquire(spec_id, false, None)?;
assert!(lock_path.exists());
assert!(lock_path.parent().unwrap().exists());
lock.release()?;
Ok(())
}
#[test]
fn test_lock_error_messages() -> Result<()> {
let _temp_dir = setup_test_env();
let spec_id = "test-error-messages";
let _lock = FileLock::acquire(spec_id, false, None)?;
let result = FileLock::acquire(spec_id, false, None);
match result {
Err(LockError::ConcurrentExecution {
spec_id: locked_spec,
pid,
created_ago,
}) => {
assert_eq!(locked_spec, spec_id);
assert_eq!(pid, std::process::id());
assert!(!created_ago.is_empty());
}
_ => panic!("Expected ConcurrentExecution error"),
}
Ok(())
}