redis-allocator 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tests/conftest.py ADDED
@@ -0,0 +1,46 @@
1
+ """Fixtures for tests."""
2
+
3
+ import pytest
4
+ import fakeredis
5
+ from redis.client import Redis
6
+ from redis_allocator.lock import RedisLock, RedisLockPool, ThreadLock, ThreadLockPool
7
+
8
+
9
+ @pytest.fixture
10
+ def redis_client():
11
+ """Create a fakeredis client for testing."""
12
+ return fakeredis.FakeRedis(decode_responses=True)
13
+
14
+
15
+ @pytest.fixture
16
+ def redis_client_raw():
17
+ """Create a fakeredis client with decode_responses=False for testing."""
18
+ return fakeredis.FakeRedis(decode_responses=False)
19
+
20
+
21
+ @pytest.fixture
22
+ def redis_lock(redis_client: Redis):
23
+ """Create a RedisLock for testing."""
24
+ return RedisLock(redis_client, 'test-lock')
25
+
26
+
27
+ @pytest.fixture
28
+ def redis_lock_pool(redis_client: Redis):
29
+ """Create a RedisLockPool for testing."""
30
+ pool = RedisLockPool(redis_client, 'test-pool')
31
+ yield pool
32
+ pool.clear()
33
+
34
+
35
+ @pytest.fixture
36
+ def thread_lock():
37
+ """Create a ThreadLock for testing."""
38
+ return ThreadLock()
39
+
40
+
41
+ @pytest.fixture
42
+ def thread_lock_pool():
43
+ """Create a ThreadLockPool for testing."""
44
+ pool = ThreadLockPool()
45
+ yield pool
46
+ pool.clear()
@@ -0,0 +1,525 @@
1
+ # flake8: noqa: F401
2
+ """Tests for the Redis-based distributed memory allocation system.
3
+
4
+ This module tests the functionality of:
5
+ 1. RedisThreadHealthCheckPool - For thread health monitoring
6
+ 2. RedisAllocator - For distributed resource allocation
7
+ 3. RedisAllocatorObject - For managing allocated resources
8
+ """
9
+ import pytest
10
+ from unittest.mock import MagicMock, patch, call
11
+ from redis import RedisError
12
+ from redis_allocator.allocator import RedisAllocator, RedisThreadHealthCheckPool, RedisAllocatorObject, RedisAllocatableClass, RedisLockPool
13
+
14
+
15
+ # Use the _TestObject naming to avoid pytest trying to collect it as a test class
16
+ class _TestObject(RedisAllocatableClass):
17
+ """Test implementation of RedisAllocatableClass for testing."""
18
+
19
+ def __init__(self):
20
+ self.config_key = None
21
+ self.config_params = None
22
+ self.closed = False
23
+
24
+ def set_config(self, key, params):
25
+ """Set configuration parameters."""
26
+ self.config_key = key
27
+ self.config_params = params
28
+
29
+ def close(self):
30
+ """Mark the object as closed."""
31
+ self.closed = True
32
+
33
+ def name(self):
34
+ """Return a name for soft binding."""
35
+ return "test_object"
36
+
37
+
38
+ @pytest.fixture
39
+ def test_object():
40
+ """Create a test object implementing RedisAllocatableClass."""
41
+ return _TestObject()
42
+
43
+
44
+ @pytest.fixture
45
+ def allocator(redis_client):
46
+ """Create a RedisAllocator instance for testing."""
47
+ alloc = RedisAllocator(
48
+ redis_client,
49
+ 'test',
50
+ 'alloc-lock',
51
+ shared=False
52
+ )
53
+ # Set up initial keys
54
+ alloc.extend(['key1', 'key2', 'key3'])
55
+ return alloc
56
+
57
+
58
+ @pytest.fixture
59
+ def shared_allocator(redis_client):
60
+ """Create a shared RedisAllocator instance for testing."""
61
+ alloc = RedisAllocator(
62
+ redis_client,
63
+ 'test',
64
+ 'shared-alloc',
65
+ shared=True
66
+ )
67
+ # Set up initial keys
68
+ alloc.extend(['key1', 'key2', 'key3'])
69
+ return alloc
70
+
71
+
72
+ @pytest.fixture
73
+ def health_checker(redis_client):
74
+ """Create a RedisThreadHealthCheckPool instance for testing."""
75
+ return RedisThreadHealthCheckPool(
76
+ redis_client,
77
+ 'test',
78
+ timeout=60
79
+ )
80
+
81
+
82
+ class TestRedisThreadHealthCheckPool:
83
+ """Tests for the RedisThreadHealthCheckPool class."""
84
+
85
+ def test_initialization(self, health_checker, redis_client):
86
+ """Test that initialization correctly registers the thread and sets up monitoring."""
87
+ # Initialization should register the current thread
88
+ assert health_checker.current_thread_id is not None
89
+ # Initialization calls update and extend, no need to check Redis calls directly
90
+ # since we're testing the object's behavior, not implementation details
91
+ assert hasattr(health_checker, 'timeout')
92
+
93
+ def test_update(self, health_checker, redis_client):
94
+ """Test that update refreshes the thread's health status."""
95
+ # Override the parent class's update method to verify our object behavior
96
+ with patch.object(RedisLockPool, 'update') as mock_update:
97
+ # Call update
98
+ health_checker.update()
99
+
100
+ # Should call the parent's update method with thread ID and timeout
101
+ mock_update.assert_called_once_with(health_checker.current_thread_id, timeout=health_checker.timeout)
102
+
103
+ def test_finalize(self, health_checker, redis_client):
104
+ """Test that finalize cleans up thread resources."""
105
+ # Override the parent class's methods to verify our object behavior
106
+ with patch.object(RedisLockPool, 'shrink') as mock_shrink:
107
+ with patch.object(RedisLockPool, 'unlock') as mock_unlock:
108
+ # Call finalize
109
+ health_checker.finalize()
110
+
111
+ # Should call shrink with thread ID
112
+ mock_shrink.assert_called_once_with([health_checker.current_thread_id])
113
+ # Should call unlock with thread ID
114
+ mock_unlock.assert_called_once_with(health_checker.current_thread_id)
115
+
116
+ def test_custom_timeout(self, redis_client):
117
+ """Test initialization with a custom timeout value."""
118
+ custom_timeout = 120
119
+ checker = RedisThreadHealthCheckPool(redis_client, 'test', timeout=custom_timeout)
120
+ assert checker.timeout == custom_timeout
121
+
122
+ def test_multiple_initialize_calls(self, health_checker):
123
+ """Test calling initialize multiple times."""
124
+ with patch.object(RedisLockPool, 'update') as mock_update:
125
+ with patch.object(RedisLockPool, 'extend') as mock_extend:
126
+ # Call initialize again
127
+ health_checker.initialize()
128
+ health_checker.initialize()
129
+
130
+ # Should have called update and extend each time
131
+ assert mock_update.call_count == 2
132
+ assert mock_extend.call_count == 2
133
+
134
+
135
+ class TestRedisAllocatorObject:
136
+ """Tests for the RedisAllocatorObject class."""
137
+
138
+ def test_initialization(self, allocator, test_object):
139
+ """Test that initialization correctly sets up the object."""
140
+ # Create a test params dict
141
+ params = {"param1": "value1", "param2": "value2"}
142
+
143
+ # Create a RedisAllocatorObject
144
+ obj = RedisAllocatorObject(allocator, "test_key", test_object, params)
145
+
146
+ # Verify properties
147
+ assert obj._allocator == allocator
148
+ assert obj.key == "test_key"
149
+ assert obj.obj == test_object
150
+ assert obj.params == params
151
+
152
+ # Verify set_config was called on the wrapped object
153
+ assert test_object.config_key == "test_key"
154
+ assert test_object.config_params == params
155
+
156
+ def test_initialization_with_defaults(self, allocator):
157
+ """Test initialization with default None values."""
158
+ # Create a RedisAllocatorObject with default None values
159
+ obj = RedisAllocatorObject(allocator, "test_key")
160
+
161
+ # Verify properties
162
+ assert obj._allocator == allocator
163
+ assert obj.key == "test_key"
164
+ assert obj.obj is None
165
+ assert obj.params is None
166
+
167
+ def test_update(self, allocator, test_object):
168
+ """Test the update method (renamed from lock)."""
169
+ # Create a RedisAllocatorObject
170
+ obj = RedisAllocatorObject(allocator, "test_key", test_object, {})
171
+
172
+ # Reset mock
173
+ allocator.update = MagicMock()
174
+
175
+ # Call update with positive timeout
176
+ obj.update(60)
177
+
178
+ # Verify update was called
179
+ allocator.update.assert_called_once_with("test_key", timeout=60)
180
+
181
+ def test_update_with_zero_timeout(self, allocator, test_object):
182
+ """Test update with zero timeout, which should free the object."""
183
+ # Create a RedisAllocatorObject
184
+ obj = RedisAllocatorObject(allocator, "test_key", test_object, {})
185
+
186
+ # Reset mocks
187
+ allocator.update = MagicMock()
188
+ allocator.free = MagicMock()
189
+
190
+ # Call update with zero timeout
191
+ obj.update(0)
192
+
193
+ # Verify free was called instead of update
194
+ allocator.update.assert_not_called()
195
+ allocator.free.assert_called_once_with(obj)
196
+
197
+ def test_close(self, allocator, test_object):
198
+ """Test the close method."""
199
+ # Create a RedisAllocatorObject
200
+ obj = RedisAllocatorObject(allocator, "test_key", test_object, {})
201
+
202
+ # Call close
203
+ obj.close()
204
+
205
+ # Verify close was called on the wrapped object
206
+ assert test_object.closed
207
+
208
+ def test_close_with_none_object(self, allocator):
209
+ """Test the close method with None object."""
210
+ # Create a RedisAllocatorObject with None object
211
+ obj = RedisAllocatorObject(allocator, "test_key")
212
+
213
+ # Call close should not raise any exception
214
+ obj.close()
215
+
216
+ def test_del(self, allocator, test_object):
217
+ """Test the __del__ method."""
218
+ # Create a RedisAllocatorObject
219
+ obj = RedisAllocatorObject(allocator, "test_key", test_object, {})
220
+
221
+ # Patch close method to verify it gets called
222
+ obj.close = MagicMock()
223
+
224
+ # Simulate __del__ being called
225
+ obj.__del__()
226
+
227
+ # Verify close was called
228
+ obj.close.assert_called_once()
229
+
230
+
231
+ class TestRedisAllocator:
232
+ """Tests for the RedisAllocator class."""
233
+
234
+ def test_initialization(self, redis_client):
235
+ """Test the initialization of RedisAllocator."""
236
+ allocator = RedisAllocator(redis_client, 'test', 'alloc-lock')
237
+
238
+ # Should have an empty WeakValueDictionary for objects
239
+ assert len(allocator.objects) == 0
240
+ # Should be initialized with default values
241
+ assert allocator.shared is False
242
+ # Should have default soft_bind_timeout
243
+ assert allocator.soft_bind_timeout == 3600
244
+
245
+ def test_initialization_with_custom_values(self, redis_client):
246
+ """Test initialization with custom values."""
247
+ eps = 1e-8
248
+ allocator = RedisAllocator(
249
+ redis_client,
250
+ 'custom_prefix',
251
+ suffix='custom_suffix',
252
+ eps=eps,
253
+ shared=True
254
+ )
255
+
256
+ # Should have custom values
257
+ assert allocator.prefix == 'custom_prefix'
258
+ assert allocator.suffix == 'custom_suffix'
259
+ assert allocator.eps == eps
260
+ assert allocator.shared is True
261
+
262
+ def test_object_key_non_shared(self, allocator, test_object):
263
+ """Test the object_key method in non-shared mode."""
264
+ # In non-shared mode, should return the key as is
265
+ allocator.shared = False
266
+ result = allocator.object_key("test_key", test_object)
267
+ assert result == "test_key"
268
+
269
+ def test_object_key_shared(self, allocator, test_object):
270
+ """Test the object_key method in shared mode."""
271
+ # In shared mode, should return key:obj
272
+ allocator.shared = True
273
+ result = allocator.object_key("test_key", test_object)
274
+ assert result == f"test_key:{test_object}"
275
+
276
+ def test_object_key_with_none(self, allocator):
277
+ """Test the object_key method with None object."""
278
+ # With None object, should still work
279
+ allocator.shared = True
280
+ result = allocator.object_key("test_key", None)
281
+ assert result == "test_key:None"
282
+
283
+ allocator.shared = False
284
+ result = allocator.object_key("test_key", None)
285
+ assert result == "test_key"
286
+
287
+ def test_extend(self, allocator, redis_client):
288
+ """Test the extend method."""
289
+ # Clear any existing data
290
+ redis_client.flushall()
291
+
292
+ # Call extend
293
+ allocator.extend(["key4", "key5"])
294
+
295
+ # Verify keys were added
296
+ assert "key4" in allocator
297
+ assert "key5" in allocator
298
+
299
+ def test_extend_empty(self, allocator, redis_client):
300
+ """Test extend with empty keys."""
301
+ # Clear any existing data
302
+ redis_client.flushall()
303
+
304
+ # Call extend with empty list
305
+ allocator.extend([])
306
+ allocator.extend(None)
307
+
308
+ # No keys should be added
309
+ assert len(list(allocator.keys())) == 0
310
+
311
+ def test_shrink(self, allocator, redis_client):
312
+ """Test the shrink method."""
313
+ # Clear any existing data
314
+ redis_client.flushall()
315
+
316
+ # Add some keys first
317
+ allocator.extend(["key1", "key2", "key3"])
318
+
319
+ # Call shrink
320
+ allocator.shrink(["key1", "key2"])
321
+
322
+ # Verify keys were removed
323
+ assert "key1" not in allocator
324
+ assert "key2" not in allocator
325
+ assert "key3" in allocator
326
+
327
+ def test_shrink_empty(self, allocator, redis_client):
328
+ """Test shrink with empty keys."""
329
+ # Clear any existing data
330
+ redis_client.flushall()
331
+
332
+ # Add some keys first
333
+ allocator.extend(["key1", "key2"])
334
+
335
+ # Call shrink with empty list
336
+ allocator.shrink([])
337
+ allocator.shrink(None)
338
+
339
+ # Keys should remain unchanged
340
+ assert "key1" in allocator
341
+ assert "key2" in allocator
342
+
343
+ def test_assign(self, allocator, redis_client):
344
+ """Test the assign method."""
345
+ # Clear any existing data
346
+ redis_client.flushall()
347
+
348
+ # Add some initial keys
349
+ allocator.extend(["key1", "key2"])
350
+
351
+ # Call assign with new keys
352
+ allocator.assign(["key3", "key4"])
353
+
354
+ # Verify old keys are gone and new keys are present
355
+ assert "key1" not in allocator
356
+ assert "key2" not in allocator
357
+ assert "key3" in allocator
358
+ assert "key4" in allocator
359
+
360
+ # Call assign with None
361
+ allocator.assign(None)
362
+
363
+ # All keys should be gone
364
+ assert len(list(allocator.keys())) == 0
365
+
366
+ def test_assign_empty(self, allocator, redis_client):
367
+ """Test assign with empty keys."""
368
+ # Clear any existing data
369
+ redis_client.flushall()
370
+
371
+ # Add some initial keys
372
+ allocator.extend(["key1", "key2"])
373
+
374
+ # Call assign with empty list
375
+ allocator.assign([])
376
+
377
+ # All keys should be gone
378
+ assert len(list(allocator.keys())) == 0
379
+
380
+ def test_clear(self, allocator, redis_client):
381
+ """Test the clear method."""
382
+ # Clear any existing data
383
+ redis_client.flushall()
384
+
385
+ # Add some keys
386
+ allocator.extend(["key1", "key2"])
387
+
388
+ # Call clear
389
+ allocator.clear()
390
+
391
+ # All keys should be gone
392
+ assert len(list(allocator.keys())) == 0
393
+
394
+ def test_redis_error_in_clear(self, allocator, redis_client):
395
+ """Test handling Redis errors in clear."""
396
+ # Clear any existing data
397
+ redis_client.flushall()
398
+
399
+ # Add some keys
400
+ allocator.extend(["key1", "key2"])
401
+
402
+ # Mock Redis error
403
+ redis_client.delete = lambda *args: (_ for _ in ()).throw(RedisError("Test error"))
404
+
405
+ # Call clear should raise RedisError
406
+ with pytest.raises(RedisError):
407
+ allocator.clear()
408
+
409
+ def test_keys(self, allocator, redis_client):
410
+ """Test the keys method."""
411
+ # Clear any existing data
412
+ redis_client.flushall()
413
+
414
+ # Add some keys
415
+ allocator.extend(["key1", "key2", "key3"])
416
+
417
+ # Get keys
418
+ result = list(allocator.keys())
419
+
420
+ # Verify we got all keys
421
+ assert set(result) == {"key1", "key2", "key3"}
422
+
423
+ def test_redis_error_in_keys(self, allocator, redis_client):
424
+ """Test handling Redis errors in keys."""
425
+ # Clear any existing data
426
+ redis_client.flushall()
427
+
428
+ # Add some keys
429
+ allocator.extend(["key1", "key2"])
430
+
431
+ # Mock Redis error
432
+ redis_client.hkeys = lambda *args: (_ for _ in ()).throw(RedisError("Test error"))
433
+
434
+ # Getting keys should raise RedisError
435
+ with pytest.raises(RedisError):
436
+ list(allocator.keys())
437
+
438
+ def test_contains(self, allocator, redis_client):
439
+ """Test the __contains__ method."""
440
+ # Clear any existing data
441
+ redis_client.flushall()
442
+
443
+ # Add some keys
444
+ allocator.extend(["key1", "key2"])
445
+
446
+ # Check containment
447
+ assert "key1" in allocator
448
+ assert "key2" in allocator
449
+ assert "key3" not in allocator
450
+
451
+ def test_redis_error_in_contains(self, allocator, redis_client):
452
+ """Test handling Redis errors in __contains__."""
453
+ # Clear any existing data
454
+ redis_client.flushall()
455
+
456
+ # Add some keys
457
+ allocator.extend(["key1", "key2"])
458
+
459
+ # Mock Redis error
460
+ redis_client.hexists = lambda *args: (_ for _ in ()).throw(RedisError("Test error"))
461
+
462
+ # Checking containment should raise RedisError
463
+ with pytest.raises(RedisError):
464
+ "key1" in allocator
465
+
466
+ def test_update_soft_bind(self, allocator, redis_client):
467
+ """Test the update_soft_bind method."""
468
+ # Set up mock
469
+ allocator.update = MagicMock()
470
+
471
+ # Call update_soft_bind
472
+ allocator.update_soft_bind("test_name", "test_key")
473
+
474
+ # Verify update was called with the right parameters
475
+ allocator.update.assert_called_once_with(
476
+ allocator._soft_bind_name("test_name"),
477
+ "test_key",
478
+ timeout=allocator.soft_bind_timeout
479
+ )
480
+
481
+ def test_unbind_soft_bind(self, allocator, redis_client):
482
+ """Test the unbind_soft_bind method."""
483
+ # Set up mock
484
+ allocator.unlock = MagicMock()
485
+
486
+ # Call unbind_soft_bind
487
+ allocator.unbind_soft_bind("test_name")
488
+
489
+ # Verify unlock was called with the right parameter
490
+ allocator.unlock.assert_called_once_with(allocator._soft_bind_name("test_name"))
491
+
492
+ def test_soft_bind_with_empty_name(self, allocator):
493
+ """Test soft bind methods with empty name."""
494
+ # Set up mocks
495
+ allocator.update = MagicMock()
496
+ allocator.unlock = MagicMock()
497
+
498
+ # Call methods with empty name
499
+ allocator.update_soft_bind("", "test_key")
500
+ allocator.unbind_soft_bind("")
501
+
502
+ # Should still call the underlying methods with empty string
503
+ allocator.update.assert_called_once()
504
+ allocator.unlock.assert_called_once()
505
+
506
+ # The soft bind name should be generated even with empty string
507
+ assert allocator._soft_bind_name("") != ""
508
+
509
+ def test_shared_vs_non_shared_allocation(self, allocator, shared_allocator):
510
+ """Test difference between shared and non-shared allocation."""
511
+ # Set up mocks for both allocators
512
+ allocator._malloc_script = MagicMock(return_value="key1")
513
+ shared_allocator._malloc_script = MagicMock(return_value="key1")
514
+
515
+ # Check the malloc_lua_script property for both allocators
516
+ non_shared_script = allocator._malloc_lua_script
517
+ shared_script = shared_allocator._malloc_lua_script
518
+
519
+ # The scripts should be different, with shared=0 in non-shared and shared=1 in shared
520
+ assert "local shared = 0" in non_shared_script
521
+ assert "local shared = 1" in shared_script
522
+
523
+ # Both can allocate the same key, but behavior should differ
524
+ assert allocator.malloc_key() == "key1"
525
+ assert shared_allocator.malloc_key() == "key1"