feat: add comprehensive tests for DRWMutex and fix critical bugs

This commit is contained in:
overtrue
2025-05-27 21:21:47 +08:00
parent a95138868e
commit 9d594cbda6

View File

@@ -100,6 +100,12 @@ impl DRWMutex {
pub async fn lock_blocking(&mut self, id: &String, source: &String, is_read_lock: bool, opts: &Options) -> bool {
let locker_len = self.lockers.len();
// Handle edge case: no lockers available
if locker_len == 0 {
return false;
}
let mut tolerance = locker_len / 2;
let mut quorum = locker_len - tolerance;
if !is_read_lock {
@@ -113,7 +119,9 @@ impl DRWMutex {
}
info!("lockBlocking {}/{} for {:?}: lockType readLock({}), additional opts: {:?}, quorum: {}, tolerance: {}, lockClients: {}\n", id, source, self.names, is_read_lock, opts, quorum, tolerance, locker_len);
tolerance = locker_len - quorum;
// Recalculate tolerance after potential quorum adjustment
// Use saturating_sub to prevent underflow
tolerance = locker_len.saturating_sub(quorum);
let mut attempt = 0;
let mut locks = vec!["".to_string(); self.lockers.len()];
@@ -293,10 +301,19 @@ fn check_failed_unlocks(locks: &[String], tolerance: usize) -> bool {
}
});
// Handle edge case: if tolerance is greater than or equal to locks.len(),
// we can tolerate all failures, so return false (no critical failure)
if tolerance >= locks.len() {
return false;
}
// Special case: when locks.len() - tolerance == tolerance (i.e., locks.len() == 2 * tolerance)
// This happens when we have an even number of lockers and tolerance is exactly half
if locks.len() - tolerance == tolerance {
return un_locks_failed >= tolerance;
}
// Normal case: failure if more than tolerance unlocks failed
un_locks_failed > tolerance
}
@@ -353,3 +370,787 @@ fn check_quorum_locked(locks: &[String], quorum: usize) -> bool {
count >= quorum
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use common::error::{Error, Result};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
// Mock locker for testing
#[derive(Debug, Clone)]
struct MockLocker {
id: String,
state: Arc<Mutex<MockLockerState>>,
}
#[derive(Debug, Default)]
struct MockLockerState {
locks: HashMap<String, String>, // uid -> owner
read_locks: HashMap<String, String>, // uid -> owner
should_fail: bool,
is_online: bool,
}
impl MockLocker {
fn new(id: String) -> Self {
Self {
id,
state: Arc::new(Mutex::new(MockLockerState {
is_online: true,
..Default::default()
})),
}
}
fn set_should_fail(&self, should_fail: bool) {
self.state.lock().unwrap().should_fail = should_fail;
}
fn set_online(&self, online: bool) {
self.state.lock().unwrap().is_online = online;
}
fn get_lock_count(&self) -> usize {
self.state.lock().unwrap().locks.len()
}
fn get_read_lock_count(&self) -> usize {
self.state.lock().unwrap().read_locks.len()
}
fn has_lock(&self, uid: &str) -> bool {
self.state.lock().unwrap().locks.contains_key(uid)
}
fn has_read_lock(&self, uid: &str) -> bool {
self.state.lock().unwrap().read_locks.contains_key(uid)
}
}
#[async_trait]
impl Locker for MockLocker {
async fn lock(&mut self, args: &LockArgs) -> Result<bool> {
let mut state = self.state.lock().unwrap();
if state.should_fail {
return Err(Error::from_string("Mock lock failure"));
}
if !state.is_online {
return Err(Error::from_string("Mock locker offline"));
}
// Check if already locked
if state.locks.contains_key(&args.uid) {
return Ok(false);
}
state.locks.insert(args.uid.clone(), args.owner.clone());
Ok(true)
}
async fn unlock(&mut self, args: &LockArgs) -> Result<bool> {
let mut state = self.state.lock().unwrap();
if state.should_fail {
return Err(Error::from_string("Mock unlock failure"));
}
Ok(state.locks.remove(&args.uid).is_some())
}
async fn rlock(&mut self, args: &LockArgs) -> Result<bool> {
let mut state = self.state.lock().unwrap();
if state.should_fail {
return Err(Error::from_string("Mock rlock failure"));
}
if !state.is_online {
return Err(Error::from_string("Mock locker offline"));
}
// Check if write lock exists
if state.locks.contains_key(&args.uid) {
return Ok(false);
}
state.read_locks.insert(args.uid.clone(), args.owner.clone());
Ok(true)
}
async fn runlock(&mut self, args: &LockArgs) -> Result<bool> {
let mut state = self.state.lock().unwrap();
if state.should_fail {
return Err(Error::from_string("Mock runlock failure"));
}
Ok(state.read_locks.remove(&args.uid).is_some())
}
async fn refresh(&mut self, _args: &LockArgs) -> Result<bool> {
let state = self.state.lock().unwrap();
if state.should_fail {
return Err(Error::from_string("Mock refresh failure"));
}
Ok(true)
}
async fn force_unlock(&mut self, args: &LockArgs) -> Result<bool> {
let mut state = self.state.lock().unwrap();
let removed_lock = state.locks.remove(&args.uid).is_some();
let removed_read_lock = state.read_locks.remove(&args.uid).is_some();
Ok(removed_lock || removed_read_lock)
}
async fn close(&self) {}
async fn is_online(&self) -> bool {
self.state.lock().unwrap().is_online
}
async fn is_local(&self) -> bool {
true
}
}
fn create_mock_lockers(count: usize) -> Vec<LockApi> {
// For testing, we'll use Local lockers which use the global local server
(0..count).map(|_| LockApi::Local).collect()
}
#[test]
fn test_drw_mutex_new() {
let names = vec!["resource1".to_string(), "resource2".to_string()];
let lockers = create_mock_lockers(3);
let mutex = DRWMutex::new("owner1".to_string(), names.clone(), lockers);
assert_eq!(mutex.owner, "owner1");
assert_eq!(mutex.names.len(), 2);
assert_eq!(mutex.lockers.len(), 3);
assert_eq!(mutex.write_locks.len(), 3);
assert_eq!(mutex.read_locks.len(), 3);
assert_eq!(mutex.refresh_interval, DRW_MUTEX_REFRESH_INTERVAL);
assert_eq!(mutex.lock_retry_min_interval, LOCK_RETRY_MIN_INTERVAL);
// Names should be sorted
let mut expected_names = names;
expected_names.sort();
assert_eq!(mutex.names, expected_names);
}
#[test]
fn test_drw_mutex_new_empty_names() {
let names = vec![];
let lockers = create_mock_lockers(1);
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
assert_eq!(mutex.names.len(), 0);
assert_eq!(mutex.lockers.len(), 1);
}
#[test]
fn test_drw_mutex_new_single_locker() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(1);
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
assert_eq!(mutex.lockers.len(), 1);
assert_eq!(mutex.write_locks.len(), 1);
assert_eq!(mutex.read_locks.len(), 1);
}
#[test]
fn test_is_locked_function() {
assert!(!is_locked(""));
assert!(is_locked("some-uid"));
assert!(is_locked("any-non-empty-string"));
}
#[test]
fn test_granted_is_locked() {
let granted_empty = Granted {
index: 0,
lock_uid: "".to_string(),
};
assert!(!granted_empty.is_locked());
let granted_locked = Granted {
index: 1,
lock_uid: "test-uid".to_string(),
};
assert!(granted_locked.is_locked());
}
#[test]
fn test_drw_mutex_is_locked() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(2);
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
// Initially not locked
assert!(!mutex.is_locked());
assert!(!mutex.is_r_locked());
// Set write locks
mutex.write_locks[0] = "test-uid".to_string();
assert!(mutex.is_locked());
assert!(!mutex.is_r_locked());
// Clear write locks, set read locks
mutex.write_locks[0] = "".to_string();
mutex.read_locks[1] = "read-uid".to_string();
assert!(!mutex.is_locked());
assert!(mutex.is_r_locked());
}
#[test]
fn test_options_debug() {
let opts = Options {
timeout: Duration::from_secs(5),
retry_interval: Duration::from_millis(100),
};
let debug_str = format!("{:?}", opts);
assert!(debug_str.contains("timeout"));
assert!(debug_str.contains("retry_interval"));
}
#[test]
fn test_check_quorum_locked() {
// Test with empty locks
assert!(!check_quorum_locked(&[], 1));
// Test with all empty locks
let locks = vec!["".to_string(), "".to_string(), "".to_string()];
assert!(!check_quorum_locked(&locks, 1));
assert!(!check_quorum_locked(&locks, 2));
// Test with some locks
let locks = vec!["uid1".to_string(), "".to_string(), "uid3".to_string()];
assert!(check_quorum_locked(&locks, 1));
assert!(check_quorum_locked(&locks, 2));
assert!(!check_quorum_locked(&locks, 3));
// Test with all locks
let locks = vec!["uid1".to_string(), "uid2".to_string(), "uid3".to_string()];
assert!(check_quorum_locked(&locks, 1));
assert!(check_quorum_locked(&locks, 2));
assert!(check_quorum_locked(&locks, 3));
assert!(!check_quorum_locked(&locks, 4));
}
#[test]
fn test_check_failed_unlocks() {
// Test with empty locks
assert!(!check_failed_unlocks(&[], 0)); // tolerance >= locks.len(), so no critical failure
assert!(!check_failed_unlocks(&[], 1)); // tolerance >= locks.len(), so no critical failure
// Test with all unlocked
let locks = vec!["".to_string(), "".to_string(), "".to_string()];
assert!(!check_failed_unlocks(&locks, 1)); // 0 failed <= tolerance 1
assert!(!check_failed_unlocks(&locks, 2)); // 0 failed <= tolerance 2
// Test with some failed unlocks
let locks = vec!["uid1".to_string(), "".to_string(), "uid3".to_string()];
assert!(check_failed_unlocks(&locks, 1)); // 2 failed > tolerance 1
assert!(!check_failed_unlocks(&locks, 2)); // 2 failed <= tolerance 2
// Test special case: locks.len() - tolerance == tolerance
// This means locks.len() == 2 * tolerance
let locks = vec!["uid1".to_string(), "uid2".to_string()]; // len = 2
let tolerance = 1; // 2 - 1 == 1
assert!(check_failed_unlocks(&locks, tolerance)); // 2 failed >= tolerance 1
let locks = vec!["".to_string(), "uid2".to_string()]; // len = 2, 1 failed
assert!(check_failed_unlocks(&locks, tolerance)); // 1 failed >= tolerance 1
let locks = vec!["".to_string(), "".to_string()]; // len = 2, 0 failed
assert!(!check_failed_unlocks(&locks, tolerance)); // 0 failed < tolerance 1
}
#[test]
fn test_check_failed_unlocks_edge_cases() {
// Test with zero tolerance
let locks = vec!["uid1".to_string()];
assert!(check_failed_unlocks(&locks, 0)); // 1 failed > tolerance 0
// Test with tolerance equal to lock count
let locks = vec!["uid1".to_string(), "uid2".to_string()];
assert!(!check_failed_unlocks(&locks, 2)); // 2 failed <= tolerance 2
// Test with tolerance greater than lock count
let locks = vec!["uid1".to_string()];
assert!(!check_failed_unlocks(&locks, 5)); // 1 failed <= tolerance 5
}
// Async tests using the local locker infrastructure
#[tokio::test]
async fn test_drw_mutex_lock_basic_functionality() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(1); // Single locker for simplicity
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
let id = "test-lock-id".to_string();
let source = "test-source".to_string();
let opts = Options {
timeout: Duration::from_secs(1),
retry_interval: Duration::from_millis(10),
};
// Test get_lock (result depends on local locker state)
let result = mutex.get_lock(&id, &source, &opts).await;
// Just ensure the method doesn't panic and returns a boolean
assert!(result == true || result == false);
// If lock was acquired, test unlock
if result {
assert!(mutex.is_locked(), "Mutex should be in locked state");
mutex.un_lock().await;
assert!(!mutex.is_locked(), "Mutex should be unlocked after un_lock");
}
}
#[tokio::test]
async fn test_drw_mutex_rlock_basic_functionality() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(1); // Single locker for simplicity
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
let id = "test-rlock-id".to_string();
let source = "test-source".to_string();
let opts = Options {
timeout: Duration::from_secs(1),
retry_interval: Duration::from_millis(10),
};
// Test get_r_lock (result depends on local locker state)
let result = mutex.get_r_lock(&id, &source, &opts).await;
// Just ensure the method doesn't panic and returns a boolean
assert!(result == true || result == false);
// If read lock was acquired, test runlock
if result {
assert!(mutex.is_r_locked(), "Mutex should be in read locked state");
mutex.un_r_lock().await;
assert!(!mutex.is_r_locked(), "Mutex should be unlocked after un_r_lock");
}
}
#[tokio::test]
async fn test_drw_mutex_lock_with_multiple_lockers() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(3); // 3 lockers, need quorum of 2
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
let id = "test-lock-id".to_string();
let source = "test-source".to_string();
let opts = Options {
timeout: Duration::from_secs(1),
retry_interval: Duration::from_millis(10),
};
// With 3 local lockers, the quorum calculation should be:
// tolerance = 3 / 2 = 1
// quorum = 3 - 1 = 2
// Since it's a write lock and quorum != tolerance, quorum stays 2
// The result depends on the actual locker implementation
let result = mutex.get_lock(&id, &source, &opts).await;
// We don't assert success/failure here since it depends on the local locker state
// Just ensure the method doesn't panic and returns a boolean
assert!(result == true || result == false);
}
#[tokio::test]
async fn test_drw_mutex_unlock_without_lock() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(1);
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
// Try to unlock without having a lock - should not panic
mutex.un_lock().await;
assert!(!mutex.is_locked());
// Try to unlock read lock without having one - should not panic
mutex.un_r_lock().await;
assert!(!mutex.is_r_locked());
}
#[tokio::test]
async fn test_drw_mutex_multiple_resources() {
let names = vec![
"resource1".to_string(),
"resource2".to_string(),
"resource3".to_string(),
];
let lockers = create_mock_lockers(1);
let mut mutex = DRWMutex::new("owner1".to_string(), names.clone(), lockers);
// Names should be sorted
let mut expected_names = names;
expected_names.sort();
assert_eq!(mutex.names, expected_names);
let id = "test-lock-id".to_string();
let source = "test-source".to_string();
let opts = Options {
timeout: Duration::from_secs(1),
retry_interval: Duration::from_millis(10),
};
let result = mutex.get_lock(&id, &source, &opts).await;
// The result depends on the actual locker implementation
// Just ensure the method doesn't panic and returns a boolean
assert!(result == true || result == false);
}
#[tokio::test]
async fn test_drw_mutex_concurrent_read_locks() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(1);
let mut mutex1 = DRWMutex::new("owner1".to_string(), names.clone(), lockers.clone());
let mut mutex2 = DRWMutex::new("owner2".to_string(), names, create_mock_lockers(1));
let id1 = "test-rlock-id1".to_string();
let id2 = "test-rlock-id2".to_string();
let source = "test-source".to_string();
let opts = Options {
timeout: Duration::from_secs(1),
retry_interval: Duration::from_millis(10),
};
// Both should be able to acquire read locks
let result1 = mutex1.get_r_lock(&id1, &source, &opts).await;
let result2 = mutex2.get_r_lock(&id2, &source, &opts).await;
assert!(result1, "First read lock should succeed");
assert!(result2, "Second read lock should succeed");
}
#[tokio::test]
async fn test_send_release_with_empty_uid() {
let mut locker = LockApi::Local;
let result = send_release(&mut locker, &"".to_string(), "owner", &["resource".to_string()], false).await;
assert!(!result, "send_release should return false for empty uid");
}
#[test]
fn test_drw_mutex_debug() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(1);
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
let debug_str = format!("{:?}", mutex);
assert!(debug_str.contains("DRWMutex"));
assert!(debug_str.contains("owner"));
assert!(debug_str.contains("names"));
}
#[test]
fn test_granted_default() {
let granted = Granted::default();
assert_eq!(granted.index, 0);
assert_eq!(granted.lock_uid, "");
assert!(!granted.is_locked());
}
#[test]
fn test_granted_clone() {
let granted = Granted {
index: 5,
lock_uid: "test-uid".to_string(),
};
let cloned = granted.clone();
assert_eq!(granted.index, cloned.index);
assert_eq!(granted.lock_uid, cloned.lock_uid);
}
// Test potential bug scenarios
#[test]
fn test_potential_bug_check_failed_unlocks_logic() {
// This test highlights the potentially confusing logic in check_failed_unlocks
// Case 1: Even number of lockers
let locks = vec!["uid1".to_string(), "uid2".to_string(), "uid3".to_string(), "uid4".to_string()];
let tolerance = 2; // locks.len() / 2 = 4 / 2 = 2
// locks.len() - tolerance = 4 - 2 = 2, which equals tolerance
// So the special case applies: un_locks_failed >= tolerance
// All 4 failed unlocks
assert!(check_failed_unlocks(&locks, tolerance)); // 4 >= 2 = true
// 2 failed unlocks
let locks = vec!["uid1".to_string(), "uid2".to_string(), "".to_string(), "".to_string()];
assert!(check_failed_unlocks(&locks, tolerance)); // 2 >= 2 = true
// 1 failed unlock
let locks = vec!["uid1".to_string(), "".to_string(), "".to_string(), "".to_string()];
assert!(!check_failed_unlocks(&locks, tolerance)); // 1 >= 2 = false
// Case 2: Odd number of lockers
let locks = vec!["uid1".to_string(), "uid2".to_string(), "uid3".to_string()];
let tolerance = 1; // locks.len() / 2 = 3 / 2 = 1
// locks.len() - tolerance = 3 - 1 = 2, which does NOT equal tolerance (1)
// So the normal case applies: un_locks_failed > tolerance
// 3 failed unlocks
assert!(check_failed_unlocks(&locks, tolerance)); // 3 > 1 = true
// 2 failed unlocks
let locks = vec!["uid1".to_string(), "uid2".to_string(), "".to_string()];
assert!(check_failed_unlocks(&locks, tolerance)); // 2 > 1 = true
// 1 failed unlock
let locks = vec!["uid1".to_string(), "".to_string(), "".to_string()];
assert!(!check_failed_unlocks(&locks, tolerance)); // 1 > 1 = false
}
#[test]
fn test_quorum_calculation_edge_cases() {
// Test the quorum calculation logic that might have issues
// For 1 locker: tolerance = 0, quorum = 1
// Write lock: quorum == tolerance (1 == 0 is false), so quorum stays 1
// This seems wrong - with 1 locker, we should need that 1 locker
// For 2 lockers: tolerance = 1, quorum = 1
// Write lock: quorum == tolerance (1 == 1 is true), so quorum becomes 2
// This makes sense - we need both lockers for write lock
// For 3 lockers: tolerance = 1, quorum = 2
// Write lock: quorum == tolerance (2 == 1 is false), so quorum stays 2
// For 4 lockers: tolerance = 2, quorum = 2
// Write lock: quorum == tolerance (2 == 2 is true), so quorum becomes 3
// The logic seems to be: for write locks, if exactly half the lockers
// would be tolerance, we need one more to avoid split brain
// Let's verify this makes sense:
struct QuorumTest {
locker_count: usize,
expected_tolerance: usize,
expected_write_quorum: usize,
expected_read_quorum: usize,
}
let test_cases = vec![
QuorumTest { locker_count: 1, expected_tolerance: 0, expected_write_quorum: 1, expected_read_quorum: 1 },
QuorumTest { locker_count: 2, expected_tolerance: 1, expected_write_quorum: 2, expected_read_quorum: 1 },
QuorumTest { locker_count: 3, expected_tolerance: 1, expected_write_quorum: 2, expected_read_quorum: 2 },
QuorumTest { locker_count: 4, expected_tolerance: 2, expected_write_quorum: 3, expected_read_quorum: 2 },
QuorumTest { locker_count: 5, expected_tolerance: 2, expected_write_quorum: 3, expected_read_quorum: 3 },
];
for test_case in test_cases {
let tolerance = test_case.locker_count / 2;
let mut write_quorum = test_case.locker_count - tolerance;
let read_quorum = write_quorum;
// Apply write lock special case
if write_quorum == tolerance {
write_quorum += 1;
}
assert_eq!(tolerance, test_case.expected_tolerance,
"Tolerance mismatch for {} lockers", test_case.locker_count);
assert_eq!(write_quorum, test_case.expected_write_quorum,
"Write quorum mismatch for {} lockers", test_case.locker_count);
assert_eq!(read_quorum, test_case.expected_read_quorum,
"Read quorum mismatch for {} lockers", test_case.locker_count);
}
}
#[test]
fn test_potential_integer_overflow() {
// Test potential issues with tolerance calculation
// What happens with 0 lockers? This should probably be an error case
let locker_count = 0;
let tolerance = locker_count / 2; // 0 / 2 = 0
let quorum = locker_count - tolerance; // 0 - 0 = 0
// This would result in quorum = 0, which doesn't make sense
assert_eq!(tolerance, 0);
assert_eq!(quorum, 0);
// The code should probably validate that locker_count > 0
}
#[test]
fn test_drw_mutex_constants() {
// Test that constants are reasonable
assert!(DRW_MUTEX_REFRESH_INTERVAL.as_secs() > 0);
assert!(LOCK_RETRY_MIN_INTERVAL.as_millis() > 0);
assert!(DRW_MUTEX_REFRESH_INTERVAL > LOCK_RETRY_MIN_INTERVAL);
}
#[test]
fn test_drw_mutex_new_with_unsorted_names() {
let names = vec![
"zebra".to_string(),
"alpha".to_string(),
"beta".to_string(),
];
let lockers = create_mock_lockers(1);
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
// Names should be sorted
assert_eq!(mutex.names, vec!["alpha", "beta", "zebra"]);
}
#[test]
fn test_drw_mutex_new_with_duplicate_names() {
let names = vec![
"resource1".to_string(),
"resource2".to_string(),
"resource1".to_string(), // Duplicate
];
let lockers = create_mock_lockers(1);
let mutex = DRWMutex::new("owner1".to_string(), names, lockers);
// Should keep duplicates but sort them
assert_eq!(mutex.names, vec!["resource1", "resource1", "resource2"]);
}
#[tokio::test]
async fn test_drw_mutex_lock_and_rlock_methods() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(1);
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
let id = "test-id".to_string();
let source = "test-source".to_string();
// Test the convenience methods (lock and r_lock)
// These should not panic and should attempt to acquire locks
mutex.lock(&id, &source).await;
// Note: We can't easily test the result since these methods don't return bool
// Clear any state
mutex.un_lock().await;
// Test r_lock
mutex.r_lock(&id, &source).await;
mutex.un_r_lock().await;
}
#[tokio::test]
async fn test_drw_mutex_zero_lockers() {
let names = vec!["resource1".to_string()];
let lockers = vec![]; // No lockers
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
let id = "test-id".to_string();
let source = "test-source".to_string();
let opts = Options {
timeout: Duration::from_secs(1),
retry_interval: Duration::from_millis(10),
};
// With 0 lockers, quorum calculation:
// tolerance = 0 / 2 = 0
// quorum = 0 - 0 = 0
// This should fail because we can't achieve any quorum
let result = mutex.get_lock(&id, &source, &opts).await;
assert!(!result, "Should fail with zero lockers");
}
#[test]
fn test_check_quorum_locked_edge_cases() {
// Test with quorum 0
let locks = vec!["".to_string()];
assert!(check_quorum_locked(&locks, 0)); // 0 >= 0
// Test with quorum larger than locks
let locks = vec!["uid1".to_string()];
assert!(!check_quorum_locked(&locks, 5)); // 1 < 5
// Test with all locks but high quorum
let locks = vec!["uid1".to_string(), "uid2".to_string(), "uid3".to_string()];
assert!(!check_quorum_locked(&locks, 4)); // 3 < 4
}
#[test]
fn test_check_failed_unlocks_comprehensive() {
// Test all combinations for small lock counts
// 1 lock scenarios
assert!(!check_failed_unlocks(&["".to_string()], 0)); // 1 success, tolerance 0 -> 1 > 0 = true, but tolerance >= len, so false
assert!(!check_failed_unlocks(&["".to_string()], 1)); // tolerance >= len
assert!(!check_failed_unlocks(&["uid".to_string()], 1)); // tolerance >= len
assert!(check_failed_unlocks(&["uid".to_string()], 0)); // 1 failed > 0
// 2 lock scenarios
let two_failed = vec!["uid1".to_string(), "uid2".to_string()];
let one_failed = vec!["uid1".to_string(), "".to_string()];
let zero_failed = vec!["".to_string(), "".to_string()];
// tolerance = 0
assert!(check_failed_unlocks(&two_failed, 0)); // 2 > 0
assert!(check_failed_unlocks(&one_failed, 0)); // 1 > 0
assert!(!check_failed_unlocks(&zero_failed, 0)); // 0 > 0 = false
// tolerance = 1 (special case: 2 - 1 == 1)
assert!(check_failed_unlocks(&two_failed, 1)); // 2 >= 1
assert!(check_failed_unlocks(&one_failed, 1)); // 1 >= 1
assert!(!check_failed_unlocks(&zero_failed, 1)); // 0 >= 1 = false
// tolerance = 2
assert!(!check_failed_unlocks(&two_failed, 2)); // tolerance >= len
assert!(!check_failed_unlocks(&one_failed, 2)); // tolerance >= len
assert!(!check_failed_unlocks(&zero_failed, 2)); // tolerance >= len
}
#[test]
fn test_options_clone() {
let opts = Options {
timeout: Duration::from_secs(5),
retry_interval: Duration::from_millis(100),
};
let cloned = opts.clone();
assert_eq!(opts.timeout, cloned.timeout);
assert_eq!(opts.retry_interval, cloned.retry_interval);
}
#[tokio::test]
async fn test_drw_mutex_release_all_edge_cases() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(2);
let mut mutex = DRWMutex::new("owner1".to_string(), names, lockers);
// Test release_all with empty locks
let mut empty_locks = vec!["".to_string(), "".to_string()];
let result = mutex.release_all(1, &mut empty_locks, false).await;
assert!(result, "Should succeed when releasing empty locks");
// Test release_all with some locks
let mut some_locks = vec!["uid1".to_string(), "uid2".to_string()];
let result = mutex.release_all(1, &mut some_locks, false).await;
// This should attempt to release the locks and may succeed or fail
// depending on the local locker state
assert!(result || !result); // Just ensure it doesn't panic
}
#[test]
fn test_drw_mutex_struct_fields() {
let names = vec!["resource1".to_string()];
let lockers = create_mock_lockers(2);
let mutex = DRWMutex::new("test-owner".to_string(), names, lockers);
// Test that all fields are properly initialized
assert_eq!(mutex.owner, "test-owner");
assert_eq!(mutex.names, vec!["resource1"]);
assert_eq!(mutex.write_locks.len(), 2);
assert_eq!(mutex.read_locks.len(), 2);
assert_eq!(mutex.lockers.len(), 2);
assert!(mutex.cancel_refresh_sender.is_none());
assert_eq!(mutex.refresh_interval, DRW_MUTEX_REFRESH_INTERVAL);
assert_eq!(mutex.lock_retry_min_interval, LOCK_RETRY_MIN_INTERVAL);
// All locks should be initially empty
for lock in &mutex.write_locks {
assert!(lock.is_empty());
}
for lock in &mutex.read_locks {
assert!(lock.is_empty());
}
}
}