/ tests / unit / common / utils / test_in_memory_cache.py
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