test_in_memory_cache.py
1 """ 2 Unit tests for common/utils/in_memory_cache.py 3 Tests the InMemoryCache class for caching with TTL support. 4 """ 5 6 import pytest 7 import time 8 import asyncio 9 from solace_agent_mesh.common.utils.in_memory_cache import InMemoryCache 10 11 12 @pytest.fixture(autouse=True) 13 def clear_cache(): 14 """Clear the singleton cache before each test to ensure test isolation.""" 15 cache = InMemoryCache() 16 cache.clear() 17 yield 18 cache.clear() 19 20 21 class TestInMemoryCacheBasicOperations: 22 """Test basic cache operations.""" 23 24 def test_set_and_get(self): 25 """Test setting and getting a value.""" 26 cache = InMemoryCache() 27 cache.set("key1", "value1") 28 29 assert cache.get("key1") == "value1" 30 31 def test_get_nonexistent_key(self): 32 """Test getting a key that doesn't exist.""" 33 cache = InMemoryCache() 34 35 assert cache.get("nonexistent") is None 36 37 def test_get_with_default(self): 38 """Test getting with a default value.""" 39 cache = InMemoryCache() 40 41 result = cache.get("nonexistent", default="default_value") 42 assert result == "default_value" 43 44 def test_set_overwrites_existing(self): 45 """Test that setting overwrites existing value.""" 46 cache = InMemoryCache() 47 cache.set("key1", "value1") 48 cache.set("key1", "value2") 49 50 assert cache.get("key1") == "value2" 51 52 def test_delete_existing_key(self): 53 """Test deleting an existing key.""" 54 cache = InMemoryCache() 55 cache.set("key1", "value1") 56 57 cache.delete("key1") 58 assert cache.get("key1") is None 59 60 def test_delete_nonexistent_key(self): 61 """Test deleting a key that doesn't exist (should not raise error).""" 62 cache = InMemoryCache() 63 cache.delete("nonexistent") # Should not raise 64 65 def test_clear_cache(self): 66 """Test clearing all cache entries.""" 67 cache = InMemoryCache() 68 cache.set("key1", "value1") 69 cache.set("key2", "value2") 70 cache.set("key3", "value3") 71 72 cache.clear() 73 74 assert cache.get("key1") is None 75 assert cache.get("key2") is None 76 assert cache.get("key3") is None 77 78 def test_key_exists_via_get(self): 79 """Test checking if key exists in cache via get method.""" 80 cache = InMemoryCache() 81 cache.set("key1", "value1") 82 83 # Check existence by getting the value 84 assert cache.get("key1") is not None 85 assert cache.get("nonexistent") is None 86 87 88 class TestInMemoryCacheTTL: 89 """Test TTL (time-to-live) functionality.""" 90 91 def test_set_with_ttl(self): 92 """Test setting a value with TTL.""" 93 cache = InMemoryCache() 94 cache.set("key1", "value1", ttl=1.0) 95 96 # Should be available immediately 97 assert cache.get("key1") == "value1" 98 99 # Should expire after TTL 100 time.sleep(1.1) 101 assert cache.get("key1") is None 102 103 def test_set_without_ttl_persists(self): 104 """Test that values without TTL persist.""" 105 cache = InMemoryCache() 106 cache.set("key1", "value1") 107 108 time.sleep(0.5) 109 assert cache.get("key1") == "value1" 110 111 def test_ttl_does_not_affect_other_keys(self): 112 """Test that TTL expiration doesn't affect other keys.""" 113 cache = InMemoryCache() 114 cache.set("key1", "value1", ttl=0.5) 115 cache.set("key2", "value2") # No TTL 116 117 time.sleep(0.6) 118 119 assert cache.get("key1") is None 120 assert cache.get("key2") == "value2" 121 122 def test_update_resets_ttl(self): 123 """Test that updating a key resets its TTL.""" 124 cache = InMemoryCache() 125 cache.set("key1", "value1", ttl=2.0) 126 127 time.sleep(0.5) 128 cache.set("key1", "value2", ttl=2.0) # Reset TTL 129 130 time.sleep(0.5) 131 # Should still be available (0.5 + 0.5 = 1.0 total, but TTL was reset after 0.5s) 132 # With 2.0s TTL reset at 0.5s mark, we have 1.5s remaining after the second sleep 133 assert cache.get("key1") == "value2" 134 135 def test_zero_ttl(self): 136 """Test setting TTL to zero (should expire immediately).""" 137 cache = InMemoryCache() 138 cache.set("key1", "value1", ttl=0) 139 140 # Even with zero TTL, might be briefly available 141 # But should definitely be gone after a small delay 142 time.sleep(0.1) 143 assert cache.get("key1") is None 144 145 def test_negative_ttl(self): 146 """Test setting negative TTL (should be treated as expired).""" 147 cache = InMemoryCache() 148 cache.set("key1", "value1", ttl=-1) 149 150 assert cache.get("key1") is None 151 152 153 class TestInMemoryCacheDataTypes: 154 """Test caching different data types.""" 155 156 def test_cache_string(self): 157 """Test caching string values.""" 158 cache = InMemoryCache() 159 cache.set("key", "string value") 160 161 assert cache.get("key") == "string value" 162 163 def test_cache_integer(self): 164 """Test caching integer values.""" 165 cache = InMemoryCache() 166 cache.set("key", 42) 167 168 assert cache.get("key") == 42 169 170 def test_cache_float(self): 171 """Test caching float values.""" 172 cache = InMemoryCache() 173 cache.set("key", 3.14159) 174 175 assert cache.get("key") == 3.14159 176 177 def test_cache_list(self): 178 """Test caching list values.""" 179 cache = InMemoryCache() 180 value = [1, 2, 3, "four"] 181 cache.set("key", value) 182 183 assert cache.get("key") == value 184 185 def test_cache_dict(self): 186 """Test caching dictionary values.""" 187 cache = InMemoryCache() 188 value = {"name": "test", "count": 42} 189 cache.set("key", value) 190 191 assert cache.get("key") == value 192 193 def test_cache_none(self): 194 """Test caching None value.""" 195 cache = InMemoryCache() 196 cache.set("key", None) 197 198 # None is a valid cached value 199 assert cache.get("key") is None 200 assert cache.get("key") is None 201 202 def test_cache_boolean(self): 203 """Test caching boolean values.""" 204 cache = InMemoryCache() 205 cache.set("key_true", True) 206 cache.set("key_false", False) 207 208 assert cache.get("key_true") is True 209 assert cache.get("key_false") is False 210 211 def test_cache_complex_object(self): 212 """Test caching complex nested objects.""" 213 cache = InMemoryCache() 214 value = { 215 "nested": { 216 "list": [1, 2, {"deep": "value"}], 217 "tuple": (1, 2, 3), 218 }, 219 "set": {1, 2, 3}, 220 } 221 cache.set("key", value) 222 223 retrieved = cache.get("key") 224 assert retrieved["nested"]["list"] == [1, 2, {"deep": "value"}] 225 226 227 class TestInMemoryCacheThreadSafety: 228 """Test thread safety of cache operations.""" 229 230 def test_concurrent_set_operations(self): 231 """Test concurrent set operations from multiple threads.""" 232 import threading 233 234 cache = InMemoryCache() 235 num_threads = 10 236 operations_per_thread = 100 237 238 def set_values(thread_id): 239 for i in range(operations_per_thread): 240 cache.set(f"key_{thread_id}_{i}", f"value_{thread_id}_{i}") 241 242 threads = [] 243 for i in range(num_threads): 244 thread = threading.Thread(target=set_values, args=(i,)) 245 threads.append(thread) 246 thread.start() 247 248 for thread in threads: 249 thread.join() 250 251 # Verify all values were set 252 for thread_id in range(num_threads): 253 for i in range(operations_per_thread): 254 key = f"key_{thread_id}_{i}" 255 expected = f"value_{thread_id}_{i}" 256 assert cache.get(key) == expected 257 258 def test_concurrent_read_write(self): 259 """Test concurrent read and write operations.""" 260 import threading 261 262 cache = InMemoryCache() 263 cache.set("shared_key", 0) 264 265 def increment(): 266 for _ in range(100): 267 current = cache.get("shared_key", default=0) 268 cache.set("shared_key", current + 1) 269 270 threads = [threading.Thread(target=increment) for _ in range(5)] 271 for thread in threads: 272 thread.start() 273 for thread in threads: 274 thread.join() 275 276 # Final value should be 500 (5 threads * 100 increments) 277 # Note: Without proper locking, this might not be exactly 500 278 # but the cache should remain consistent 279 final_value = cache.get("shared_key") 280 assert isinstance(final_value, int) 281 assert final_value > 0 282 283 284 class TestInMemoryCacheEdgeCases: 285 """Test edge cases and special scenarios.""" 286 287 def test_empty_string_key(self): 288 """Test using empty string as key.""" 289 cache = InMemoryCache() 290 cache.set("", "value") 291 292 assert cache.get("") == "value" 293 294 def test_very_long_key(self): 295 """Test using very long key.""" 296 cache = InMemoryCache() 297 long_key = "k" * 10000 298 cache.set(long_key, "value") 299 300 assert cache.get(long_key) == "value" 301 302 def test_special_characters_in_key(self): 303 """Test keys with special characters.""" 304 cache = InMemoryCache() 305 special_keys = [ 306 "key with spaces", 307 "key/with/slashes", 308 "key.with.dots", 309 "key:with:colons", 310 "key@with@at", 311 ] 312 313 for key in special_keys: 314 cache.set(key, f"value_for_{key}") 315 316 for key in special_keys: 317 assert cache.get(key) == f"value_for_{key}" 318 319 def test_numeric_keys(self): 320 """Test using numeric keys.""" 321 cache = InMemoryCache() 322 cache.set(123, "value_for_123") 323 cache.set(45.67, "value_for_45.67") 324 325 assert cache.get(123) == "value_for_123" 326 assert cache.get(45.67) == "value_for_45.67" 327 328 def test_large_value(self): 329 """Test caching large values.""" 330 cache = InMemoryCache() 331 large_value = "x" * 1000000 # 1MB string 332 cache.set("large", large_value) 333 334 assert len(cache.get("large")) == 1000000 335 336 def test_many_keys(self): 337 """Test caching many keys.""" 338 cache = InMemoryCache() 339 num_keys = 1000 340 341 for i in range(num_keys): 342 cache.set(f"key_{i}", f"value_{i}") 343 344 # Verify all keys are accessible 345 for i in range(num_keys): 346 assert cache.get(f"key_{i}") == f"value_{i}" 347 348 def test_clear_with_ttl_entries(self): 349 """Test clearing cache with TTL entries.""" 350 cache = InMemoryCache() 351 cache.set("key1", "value1", ttl=10.0) 352 cache.set("key2", "value2") 353 354 cache.clear() 355 356 assert cache.get("key1") is None 357 assert cache.get("key2") is None 358 359 def test_get_expired_key_returns_none(self): 360 """Test that getting an expired key returns None.""" 361 cache = InMemoryCache() 362 cache.set("key", "value", ttl=0.1) 363 364 time.sleep(0.2) 365 366 result = cache.get("key") 367 assert result is None