LightRAG/tests/test_unified_lock_safety.py
BukeLy 3b8a1e64b7 style: apply ruff formatting fixes to test files
Apply ruff-format fixes to 6 test files to pass pre-commit checks:
- test_dimension_mismatch.py
- test_e2e_multi_instance.py
- test_no_model_suffix_safety.py
- test_postgres_migration.py
- test_unified_lock_safety.py
- test_workspace_migration_isolation.py

Changes are primarily assert statement reformatting to match ruff style guide.
2025-11-23 16:59:02 +08:00

191 lines
6.7 KiB
Python

"""
Tests for UnifiedLock safety when lock is None.
This test module verifies that UnifiedLock raises RuntimeError instead of
allowing unprotected execution when the underlying lock is None, preventing
false security and potential race conditions.
Critical Bug 1: When self._lock is None, __aenter__ used to log WARNING but
still return successfully, allowing critical sections to run without lock
protection, causing race conditions and data corruption.
Critical Bug 2: In __aexit__, when async_lock.release() fails, the error
recovery logic would attempt to release it again, causing double-release issues.
"""
import pytest
from unittest.mock import MagicMock, AsyncMock
from lightrag.kg.shared_storage import UnifiedLock
class TestUnifiedLockSafety:
"""Test suite for UnifiedLock None safety checks."""
@pytest.mark.asyncio
async def test_unified_lock_raises_on_none_async(self):
"""
Test that UnifiedLock raises RuntimeError when lock is None (async mode).
Scenario: Attempt to use UnifiedLock before initialize_share_data() is called.
Expected: RuntimeError raised, preventing unprotected critical section execution.
"""
lock = UnifiedLock(
lock=None, is_async=True, name="test_async_lock", enable_logging=False
)
with pytest.raises(
RuntimeError, match="shared data not initialized|Lock.*is None"
):
async with lock:
# This code should NEVER execute
pytest.fail(
"Code inside lock context should not execute when lock is None"
)
@pytest.mark.asyncio
async def test_unified_lock_raises_on_none_sync(self):
"""
Test that UnifiedLock raises RuntimeError when lock is None (sync mode).
Scenario: Attempt to use UnifiedLock with None lock in sync mode.
Expected: RuntimeError raised with clear error message.
"""
lock = UnifiedLock(
lock=None, is_async=False, name="test_sync_lock", enable_logging=False
)
with pytest.raises(
RuntimeError, match="shared data not initialized|Lock.*is None"
):
async with lock:
# This code should NEVER execute
pytest.fail(
"Code inside lock context should not execute when lock is None"
)
@pytest.mark.asyncio
async def test_error_message_clarity(self):
"""
Test that the error message clearly indicates the problem and solution.
Scenario: Lock is None and user tries to acquire it.
Expected: Error message mentions 'shared data not initialized' and
'initialize_share_data()'.
"""
lock = UnifiedLock(
lock=None,
is_async=True,
name="test_error_message",
enable_logging=False,
)
with pytest.raises(RuntimeError) as exc_info:
async with lock:
pass
error_message = str(exc_info.value)
# Verify error message contains helpful information
assert (
"shared data not initialized" in error_message.lower()
or "lock" in error_message.lower()
)
assert "initialize_share_data" in error_message or "None" in error_message
@pytest.mark.asyncio
async def test_aexit_no_double_release_on_async_lock_failure(self):
"""
Test that __aexit__ doesn't attempt to release async_lock twice when it fails.
Scenario: async_lock.release() fails during normal release.
Expected: Recovery logic should NOT attempt to release async_lock again,
preventing double-release issues.
This tests Bug 2 fix: async_lock_released tracking prevents double release.
"""
# Create mock locks
main_lock = MagicMock()
main_lock.acquire = MagicMock()
main_lock.release = MagicMock()
async_lock = AsyncMock()
async_lock.acquire = AsyncMock()
# Make async_lock.release() fail
release_call_count = 0
def mock_release_fail():
nonlocal release_call_count
release_call_count += 1
raise RuntimeError("Async lock release failed")
async_lock.release = MagicMock(side_effect=mock_release_fail)
# Create UnifiedLock with both locks (sync mode with async_lock)
lock = UnifiedLock(
lock=main_lock,
is_async=False,
name="test_double_release",
enable_logging=False,
)
lock._async_lock = async_lock
# Try to use the lock - should fail during __aexit__
try:
async with lock:
pass
except RuntimeError as e:
# Should get the async lock release error
assert "Async lock release failed" in str(e)
# Verify async_lock.release() was called only ONCE, not twice
assert (
release_call_count == 1
), f"async_lock.release() should be called only once, but was called {release_call_count} times"
# Main lock should have been released successfully
main_lock.release.assert_called_once()
@pytest.mark.asyncio
async def test_aexit_recovery_on_main_lock_failure(self):
"""
Test that __aexit__ recovery logic works when main lock release fails.
Scenario: main_lock.release() fails before async_lock is attempted.
Expected: Recovery logic should attempt to release async_lock to prevent
resource leaks.
This verifies the recovery logic still works correctly with async_lock_released tracking.
"""
# Create mock locks
main_lock = MagicMock()
main_lock.acquire = MagicMock()
# Make main_lock.release() fail
def mock_main_release_fail():
raise RuntimeError("Main lock release failed")
main_lock.release = MagicMock(side_effect=mock_main_release_fail)
async_lock = AsyncMock()
async_lock.acquire = AsyncMock()
async_lock.release = MagicMock()
# Create UnifiedLock with both locks (sync mode with async_lock)
lock = UnifiedLock(
lock=main_lock, is_async=False, name="test_recovery", enable_logging=False
)
lock._async_lock = async_lock
# Try to use the lock - should fail during __aexit__
try:
async with lock:
pass
except RuntimeError as e:
# Should get the main lock release error
assert "Main lock release failed" in str(e)
# Main lock release should have been attempted
main_lock.release.assert_called_once()
# Recovery logic should have attempted to release async_lock
async_lock.release.assert_called_once()