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/test_lock.py ADDED
@@ -0,0 +1,851 @@
1
+ """Tests for the RedisLock and RedisLockPool classes."""
2
+ from redis import Redis
3
+ from redis_allocator.lock import RedisLock, RedisLockPool, LockStatus, ThreadLock, ThreadLockPool
4
+ import time
5
+ import concurrent.futures
6
+ import pytest
7
+
8
+
9
+ class TestRedisLock:
10
+ """Tests for the RedisLock class."""
11
+
12
+ def test_lock(self, redis_lock: RedisLock, redis_client: Redis):
13
+ """Test the lock method."""
14
+ assert redis_lock.lock('test-key') is True
15
+ assert redis_client.exists(redis_lock._key_str('test-key'))
16
+
17
+ def test_unlock(self, redis_lock: RedisLock, redis_client: Redis):
18
+ """Test the unlock method."""
19
+ # First set the lock
20
+ redis_lock.update('test-key')
21
+ assert redis_client.exists(redis_lock._key_str('test-key'))
22
+ # Then unlock
23
+ redis_lock.unlock('test-key')
24
+ assert not redis_client.exists(redis_lock._key_str('test-key'))
25
+
26
+ def test_update(self, redis_lock: RedisLock, redis_client: Redis):
27
+ """Test the update method."""
28
+ redis_lock.update('test-key', value='2', timeout=60)
29
+ assert redis_client.get(redis_lock._key_str('test-key')) == '2'
30
+ # TTL should be close to 60
31
+ ttl = redis_client.ttl(redis_lock._key_str('test-key'))
32
+ assert 55 <= ttl <= 60
33
+
34
+ def test_lock_value(self, redis_lock: RedisLock, redis_client: Redis):
35
+ """Test the lock_value method."""
36
+ redis_client.set(redis_lock._key_str('test-key'), '120')
37
+ assert redis_lock.lock_value('test-key') == '120'
38
+
39
+ def test_is_locked(self, redis_lock: RedisLock, redis_client: Redis):
40
+ """Test the is_locked method."""
41
+ assert not redis_lock.is_locked('test-key')
42
+ redis_client.set(redis_lock._key_str('test-key'), '300')
43
+ assert redis_lock.is_locked('test-key')
44
+ assert not redis_lock.is_locked('non-existent-key')
45
+
46
+ def test_key_status(self, redis_lock: RedisLock, redis_client: Redis):
47
+ """Test the key_status method."""
48
+ # Test FREE status
49
+ assert redis_lock.key_status('free-key') == LockStatus.FREE
50
+
51
+ # Test LOCKED status
52
+ redis_client.set(redis_lock._key_str('locked-key'), '1', ex=60)
53
+ assert redis_lock.key_status('locked-key') == LockStatus.LOCKED
54
+
55
+ # Test ERROR status (permanent lock)
56
+ redis_client.set(redis_lock._key_str('error-key'), '1')
57
+ assert redis_lock.key_status('error-key') == LockStatus.ERROR
58
+
59
+ # Test UNAVAILABLE status (TTL > timeout)
60
+ redis_client.set(redis_lock._key_str('unavailable-key'), '1', ex=200)
61
+ assert redis_lock.key_status('unavailable-key', timeout=100) == LockStatus.UNAVAILABLE
62
+
63
+ def test_rlock(self, redis_lock: RedisLock, redis_client: Redis):
64
+ """Test the rlock method."""
65
+ # First test acquiring a lock
66
+ assert redis_lock.rlock('test-key', value='1') is True
67
+ assert redis_client.get(redis_lock._key_str('test-key')) == '1'
68
+
69
+ # Test acquiring the same lock with the same value
70
+ assert redis_lock.rlock('test-key', value='1') is True
71
+
72
+ # Test acquiring the same lock with a different value
73
+ assert redis_lock.rlock('test-key', value='2') is False
74
+
75
+ # Verify the original value wasn't changed
76
+ assert redis_client.get(redis_lock._key_str('test-key')) == '1'
77
+
78
+ def test_conditional_setdel_operations(self, redis_lock: RedisLock, redis_client: Redis):
79
+ """Test the conditional set/del operations."""
80
+ # Skip direct testing of _conditional_setdel as it's an internal method
81
+ # We'll test the public methods that use it instead
82
+
83
+ # Set initial value
84
+ test_key = 'cond-key'
85
+ key_str = redis_lock._key_str(test_key)
86
+ redis_client.set(key_str, '10')
87
+
88
+ # Test setgt (greater than)
89
+ assert redis_lock.setgt(key=test_key, value=15) # 15 > 10, should succeed
90
+ assert redis_client.get(key_str) == '15' # Value should be updated to 15
91
+
92
+ # Test setgt with unsuccessful condition
93
+ assert not redis_lock.setgt(key=test_key, value=5) # 5 < 15, should not succeed
94
+ assert redis_client.get(key_str) == '15' # Value should remain 15
95
+
96
+ # Test setlt (less than)
97
+ assert redis_lock.setlt(key=test_key, value=5) # 5 < 15, should succeed
98
+ assert redis_client.get(key_str) == '5' # Value should be updated to 5
99
+
100
+ # Test setlt with unsuccessful condition
101
+ assert not redis_lock.setlt(key=test_key, value=10) # 10 > 5, should not succeed
102
+ assert redis_client.get(key_str) == '5' # Value should remain 5
103
+
104
+ # Test setge (greater than or equal)
105
+ assert redis_lock.setge(key=test_key, value=5) # 5 >= 5, should succeed
106
+ assert redis_client.get(key_str) == '5' # Value should still be 5
107
+
108
+ # Test setge with unsuccessful condition
109
+ assert not redis_lock.setge(key=test_key, value=4) # 4 < 5, should not succeed
110
+ assert redis_client.get(key_str) == '5' # Value should remain 5
111
+
112
+ # Test setle (less than or equal)
113
+ assert redis_lock.setle(key=test_key, value=5) # 5 <= 5, should succeed
114
+ assert redis_client.get(key_str) == '5' # Value should remain 5
115
+
116
+ # Test setle with unsuccessful condition
117
+ assert not redis_lock.setle(key=test_key, value=6) # 6 > 5, should not succeed
118
+ assert redis_client.get(key_str) == '5' # Value should remain 5
119
+
120
+ # Test seteq (equal)
121
+ assert redis_lock.seteq(key=test_key, value=5) # 5 == 5, should succeed
122
+ assert redis_client.get(key_str) == '5' # Value should remain 5
123
+
124
+ # Test seteq with unsuccessful condition
125
+ assert not redis_lock.seteq(key=test_key, value=6) # 6 != 5, should not succeed
126
+ assert redis_client.get(key_str) == '5' # Value should remain 5
127
+
128
+ # Test setne (not equal)
129
+ assert redis_lock.setne(key=test_key, value=10) # 10 != 5, should succeed
130
+ assert redis_client.get(key_str) == '10' # Value should be updated to 10
131
+
132
+ # Test setne with unsuccessful condition
133
+ assert not redis_lock.setne(key=test_key, value=10) # 10 == 10, should not succeed
134
+ assert redis_client.get(key_str) == '10' # Value should remain 10
135
+
136
+ # Test delgt (delete if greater than)
137
+ redis_client.set(key_str, '10')
138
+ redis_lock.delgt(key=test_key, value=15) # 15 > 10, should delete
139
+ assert not redis_client.exists(key_str) # Key should be deleted
140
+
141
+ # Test dellt (delete if less than)
142
+ redis_client.set(key_str, '10')
143
+ redis_lock.dellt(key=test_key, value=5) # 5 < 10, should delete
144
+ assert not redis_client.exists(key_str) # Key should be deleted
145
+
146
+ # Test delge (delete if greater than or equal)
147
+ redis_client.set(key_str, '10')
148
+ redis_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
149
+ assert not redis_client.exists(key_str) # Key should be deleted
150
+
151
+ # Test delle (delete if less than or equal)
152
+ redis_client.set(key_str, '10')
153
+ redis_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
154
+ assert not redis_client.exists(key_str) # Key should be deleted
155
+
156
+ # Test deleq (delete if equal)
157
+ redis_client.set(key_str, '10')
158
+ redis_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
159
+ assert not redis_client.exists(key_str) # Key should be deleted
160
+
161
+ # Test delne (delete if not equal)
162
+ redis_client.set(key_str, '10')
163
+ redis_lock.delne(key=test_key, value=5) # 5 != 10, should delete
164
+ assert not redis_client.exists(key_str) # Key should be deleted
165
+
166
+ # Test with expired keys
167
+ redis_client.set(key_str, '10')
168
+ redis_lock.setgt(key=test_key, value=15, ex=30) # set with expiration
169
+ ttl = redis_client.ttl(key_str)
170
+ assert ttl > 0 and ttl <= 30 # Should have a TTL set
171
+
172
+ def test_setters_and_deleters(self, redis_lock: RedisLock, redis_client: Redis):
173
+ """Test all setter and deleter methods."""
174
+ test_key = 'op-key'
175
+ key_str = redis_lock._key_str(test_key)
176
+
177
+ # Let's test the setters first
178
+
179
+ # Set an initial value
180
+ redis_client.set(key_str, '10')
181
+
182
+ # Test setgt (doesn't meet condition)
183
+ assert not redis_lock.setgt(key=test_key, value=5)
184
+ assert redis_client.get(key_str) == '10' # Unchanged because 5 is not > 10
185
+
186
+ # Test setgt (meets condition)
187
+ assert redis_lock.setgt(key=test_key, value=15, set_value=7)
188
+ assert redis_client.get(key_str) == '7' # Changed because 15 > 10
189
+
190
+ # Test setlt (doesn't meet condition)
191
+ assert not redis_lock.setlt(key=test_key, value=10)
192
+ assert redis_client.get(key_str) == '7' # Unchanged because 10 is not < 7
193
+
194
+ # Test setlt (meets condition)
195
+ assert redis_lock.setlt(key=test_key, value=5)
196
+ assert redis_client.get(key_str) == '5' # Changed because 5 < 7
197
+
198
+ # Reset for more tests
199
+ redis_client.set(key_str, '10')
200
+
201
+ # Test setge (meets condition)
202
+ assert redis_lock.setge(key=test_key, value=10)
203
+ assert redis_client.get(key_str) == '10' # Changed because 10 >= 10
204
+
205
+ # Test setle (meets condition)
206
+ assert redis_lock.setle(key=test_key, value=10)
207
+ assert redis_client.get(key_str) == '10' # Changed because 10 <= 10
208
+
209
+ # Test seteq (meets condition)
210
+ assert redis_lock.seteq(key=test_key, value=10)
211
+ assert redis_client.get(key_str) == '10' # Changed because 10 == 10
212
+
213
+ # Test setne (doesn't meet condition)
214
+ assert not redis_lock.setne(key=test_key, value=10)
215
+ assert redis_client.get(key_str) == '10' # Unchanged because 10 is not != 10
216
+
217
+ # Test setne (meets condition)
218
+ assert redis_lock.setne(key=test_key, value=5)
219
+ assert redis_client.get(key_str) == '5' # Changed because 5 != 10
220
+
221
+ # Now test the deleters
222
+
223
+ # Reset for delete tests
224
+ redis_client.set(key_str, '10')
225
+
226
+ # Test delgt (doesn't meet condition)
227
+ assert not redis_lock.delgt(key=test_key, value=5)
228
+ assert redis_client.exists(key_str) # Key still exists because 5 is not > 10
229
+
230
+ # Test delgt (meets condition)
231
+ assert redis_lock.delgt(key=test_key, value=15)
232
+ assert not redis_client.exists(key_str) # Key deleted because 15 > 10
233
+
234
+ # Reset for more delete tests
235
+ redis_client.set(key_str, '10')
236
+
237
+ # Test dellt (doesn't meet condition)
238
+ assert not redis_lock.dellt(key=test_key, value=15)
239
+ assert redis_client.exists(key_str) # Key still exists because 15 is not < 10
240
+
241
+ # Test dellt (meets condition)
242
+ assert redis_lock.dellt(key=test_key, value=5)
243
+ assert not redis_client.exists(key_str) # Key deleted because 5 < 10
244
+
245
+ # Test delge, delle, deleq, delne
246
+ redis_client.set(key_str, '10')
247
+ assert redis_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
248
+ assert not redis_client.exists(key_str)
249
+
250
+ redis_client.set(key_str, '10')
251
+ assert redis_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
252
+ assert not redis_client.exists(key_str)
253
+
254
+ redis_client.set(key_str, '10')
255
+ assert redis_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
256
+ assert not redis_client.exists(key_str)
257
+
258
+ redis_client.set(key_str, '10')
259
+ assert redis_lock.delne(key=test_key, value=5) # 5 != 10, should delete
260
+ assert not redis_client.exists(key_str)
261
+
262
+ # Test unsuccessful deletions
263
+ redis_client.set(key_str, '10')
264
+ assert not redis_lock.delge(key=test_key, value=5) # 5 < 10, should not delete
265
+ assert redis_client.exists(key_str)
266
+
267
+ assert not redis_lock.delle(key=test_key, value=15) # 15 > 10, should not delete
268
+ assert redis_client.exists(key_str)
269
+
270
+ assert not redis_lock.deleq(key=test_key, value=11) # 11 != 10, should not delete
271
+ assert redis_client.exists(key_str)
272
+
273
+ assert not redis_lock.delne(key=test_key, value=10) # 10 == 10, should not delete
274
+ assert redis_client.exists(key_str)
275
+
276
+ def test_equality_and_hash(self, redis_client: Redis):
277
+ """Test equality and hash methods."""
278
+ # Create two identical locks
279
+ lock1 = RedisLock(redis_client, 'test')
280
+ lock2 = RedisLock(redis_client, 'test')
281
+
282
+ # Create a different lock
283
+ lock3 = RedisLock(redis_client, 'different')
284
+
285
+ # Test equality
286
+ assert lock1 == lock2
287
+ assert lock1 != lock3
288
+ assert lock1 != "not a lock"
289
+
290
+ # Test hash
291
+ lock_set = {lock1, lock2, lock3}
292
+ assert len(lock_set) == 2 # lock1 and lock2 should hash to the same value
293
+
294
+ def test_invalid_operator(self, redis_lock: RedisLock):
295
+ """Test that an invalid operator raises a ValueError."""
296
+ with pytest.raises(ValueError, match="Invalid operator"):
297
+ redis_lock._conditional_setdel_lua_script("invalid", 0.001)
298
+
299
+ def test_timeout_types(self, redis_lock: RedisLock, redis_client: Redis):
300
+ """Test different timeout types."""
301
+ from datetime import timedelta
302
+
303
+ # Test with timedelta
304
+ test_key = 'timeout-key'
305
+ key_str = redis_lock._key_str(test_key)
306
+
307
+ # Use timedelta for timeout
308
+ redis_lock.update(test_key, value='1', timeout=timedelta(seconds=30))
309
+ ttl = redis_client.ttl(key_str)
310
+ assert 25 <= ttl <= 30 # Allow a small margin
311
+
312
+ # Use None for timeout (should use a very long timeout)
313
+ redis_lock.update(test_key, value='2', timeout=None)
314
+ ttl = redis_client.ttl(key_str)
315
+ assert ttl == -1 # No expiration
316
+
317
+ def test_decoder_assertion(self, redis_client_raw: Redis):
318
+ """Test that the RedisLock constructor asserts that decode_responses is enabled."""
319
+ # redis_client_raw is a client with decode_responses=False
320
+ with pytest.raises(AssertionError, match="Redis must be configured to decode responses"):
321
+ RedisLock(redis_client_raw, "test")
322
+
323
+
324
+ class TestRedisLockPool:
325
+ """Tests for the RedisLockPool class."""
326
+
327
+ def test_keys(self, redis_lock_pool: RedisLockPool):
328
+ """Test the keys method."""
329
+ # Add some keys to the pool
330
+ redis_lock_pool.assign(['key1', 'key2'])
331
+ keys = redis_lock_pool.keys()
332
+ assert sorted(list(keys)) == ['key1', 'key2']
333
+
334
+ def test_extend(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
335
+ """Test the extend method with keys."""
336
+ redis_lock_pool.extend(['key1', 'key2'])
337
+
338
+ # Verify keys were added
339
+ assert 'key1' in redis_lock_pool
340
+ assert 'key2' in redis_lock_pool
341
+
342
+ def test_shrink(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
343
+ """Test the shrink method."""
344
+ # First add keys
345
+ redis_lock_pool.extend(['key1', 'key2', 'key3'])
346
+
347
+ # Then shrink
348
+ redis_lock_pool.shrink(['key1', 'key2'])
349
+
350
+ # Verify keys were removed
351
+ assert 'key1' not in redis_lock_pool
352
+ assert 'key2' not in redis_lock_pool
353
+ assert 'key3' in redis_lock_pool
354
+
355
+ def test_clear(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
356
+ """Test the clear method."""
357
+ # First add keys
358
+ redis_lock_pool.extend(['key1', 'key2'])
359
+
360
+ # Then clear
361
+ redis_lock_pool.clear()
362
+
363
+ # Verify all keys were removed
364
+ assert len(redis_lock_pool) == 0
365
+
366
+ def test_assign(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
367
+ """Test the assign method."""
368
+ # First add some initial keys
369
+ redis_lock_pool.extend(['old1', 'old2'])
370
+
371
+ # Then assign new keys
372
+ redis_lock_pool.assign(['key1', 'key2'])
373
+
374
+ # Verify old keys were removed and new keys were added
375
+ assert 'old1' not in redis_lock_pool
376
+ assert 'old2' not in redis_lock_pool
377
+ assert 'key1' in redis_lock_pool
378
+ assert 'key2' in redis_lock_pool
379
+
380
+ def test_contains(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
381
+ """Test the __contains__ method."""
382
+ # Add a key
383
+ redis_lock_pool.extend(['key1'])
384
+
385
+ # Test __contains__
386
+ assert 'key1' in redis_lock_pool
387
+ assert 'key2' not in redis_lock_pool
388
+
389
+ def test_len(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
390
+ """Test the __len__ method."""
391
+ # Add some keys
392
+ redis_lock_pool.extend(['key1', 'key2', 'key3'])
393
+
394
+ # Test __len__
395
+ assert len(redis_lock_pool) == 3
396
+
397
+ def test_get_set_del_item(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
398
+ """Test the __getitem__, __setitem__, and __delitem__ methods."""
399
+ # First test extend to add a key
400
+ redis_lock_pool.extend(['key1'])
401
+
402
+ # Test __getitem__
403
+ assert 'key1' in redis_lock_pool
404
+
405
+ # Test shrink to remove key
406
+ redis_lock_pool.shrink(['key1'])
407
+ assert 'key1' not in redis_lock_pool
408
+
409
+ def test_health_check(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
410
+ """Test the health_check method."""
411
+ # Add some keys
412
+ redis_lock_pool.extend(['key1', 'key2', 'key3'])
413
+
414
+ # Lock some keys
415
+ redis_lock_pool.lock('key1')
416
+ redis_lock_pool.lock('key2')
417
+
418
+ # Check health
419
+ locked, free = redis_lock_pool.health_check()
420
+ assert locked == 2
421
+ assert free == 1
422
+
423
+ def test_empty_health_check(self, redis_lock_pool: RedisLockPool):
424
+ """Test health_check on an empty pool."""
425
+ # Clear the pool first
426
+ redis_lock_pool.clear()
427
+
428
+ # Check health of empty pool
429
+ locked, free = redis_lock_pool.health_check()
430
+ assert locked == 0
431
+ assert free == 0
432
+
433
+ def test_extend_empty_keys(self, redis_lock_pool: RedisLockPool):
434
+ """Test extending the pool with an empty list of keys."""
435
+ # Clear the pool first
436
+ redis_lock_pool.clear()
437
+
438
+ # Extend with empty list - should not change anything
439
+ redis_lock_pool.extend([])
440
+ assert len(redis_lock_pool) == 0
441
+
442
+ # Test with None
443
+ redis_lock_pool.extend(None)
444
+ assert len(redis_lock_pool) == 0
445
+
446
+ def test_shrink_empty_keys(self, redis_lock_pool: RedisLockPool):
447
+ """Test shrinking the pool with an empty list of keys."""
448
+ # Add some keys
449
+ redis_lock_pool.extend(['key1', 'key2'])
450
+
451
+ # Shrink with empty list - should not change anything
452
+ redis_lock_pool.shrink([])
453
+ assert len(redis_lock_pool) == 2
454
+
455
+ # Test with None - should also not change anything
456
+ redis_lock_pool.shrink(None)
457
+ assert len(redis_lock_pool) == 2
458
+
459
+ def test_assign_with_keys(self, redis_lock_pool: RedisLockPool):
460
+ """Test assigning keys to the pool."""
461
+ # Clear first
462
+ redis_lock_pool.clear()
463
+
464
+ # Test assign with actual keys
465
+ keys = ['new1', 'new2', 'new3']
466
+ redis_lock_pool.assign(keys)
467
+
468
+ # Verify all keys were added
469
+ assert len(redis_lock_pool) == 3
470
+ for key in keys:
471
+ assert key in redis_lock_pool
472
+
473
+ def test_iterate_pool(self, redis_lock_pool: RedisLockPool):
474
+ """Test iterating over the keys in the pool."""
475
+ # Add some keys
476
+ test_keys = ['key1', 'key2', 'key3']
477
+ redis_lock_pool.extend(test_keys)
478
+
479
+ # Iterate and collect keys
480
+ iterated_keys = []
481
+ for key in redis_lock_pool:
482
+ iterated_keys.append(key)
483
+
484
+ # Verify all keys were iterated
485
+ assert sorted(iterated_keys) == sorted(test_keys)
486
+
487
+ def test_assign_empty(self, redis_lock_pool: RedisLockPool):
488
+ """Test assigning an empty list of keys to the pool."""
489
+ # First add some keys
490
+ redis_lock_pool.extend(['old1', 'old2'])
491
+
492
+ # Then assign empty list
493
+ redis_lock_pool.assign([])
494
+
495
+ # Verify all keys were removed
496
+ assert len(redis_lock_pool) == 0
497
+
498
+ # Assign None
499
+ redis_lock_pool.extend(['old1'])
500
+ redis_lock_pool.assign(None)
501
+ assert len(redis_lock_pool) == 0
502
+
503
+
504
+ class TestThreadLock:
505
+ """Tests for the ThreadLock class."""
506
+
507
+ def test_lock(self, thread_lock: ThreadLock):
508
+ """Test the lock method."""
509
+ assert thread_lock.lock('test-key')
510
+ assert thread_lock.is_locked('test-key')
511
+
512
+ def test_unlock(self, thread_lock: ThreadLock):
513
+ """Test the unlock method."""
514
+ # First set the lock
515
+ thread_lock.update('test-key')
516
+ assert thread_lock.is_locked('test-key')
517
+ # Then unlock
518
+ thread_lock.unlock('test-key')
519
+ assert not thread_lock.is_locked('test-key')
520
+
521
+ def test_update(self, thread_lock: ThreadLock):
522
+ """Test the update method."""
523
+ thread_lock.update('test-key', value='2', timeout=60)
524
+ assert thread_lock.lock_value('test-key') == '2'
525
+ # TTL should be close to 60
526
+ assert thread_lock.key_status('test-key', timeout=60) == LockStatus.LOCKED
527
+
528
+ def test_lock_value(self, thread_lock: ThreadLock):
529
+ """Test the lock_value method."""
530
+ thread_lock.update('test-key', value='120')
531
+ assert thread_lock.lock_value('test-key') == '120'
532
+
533
+ def test_is_locked(self, thread_lock: ThreadLock):
534
+ """Test the is_locked method."""
535
+ assert not thread_lock.is_locked('test-key')
536
+ thread_lock.update('test-key', value='300')
537
+ assert thread_lock.is_locked('test-key')
538
+ assert not thread_lock.is_locked('non-existent-key')
539
+
540
+ def test_key_status(self, thread_lock: ThreadLock):
541
+ """Test the key_status method."""
542
+ # Test FREE status
543
+ assert thread_lock.key_status('free-key') == LockStatus.FREE
544
+
545
+ # Test LOCKED status
546
+ thread_lock.update('locked-key', value='1', timeout=60)
547
+ assert thread_lock.key_status('locked-key') == LockStatus.LOCKED
548
+
549
+ # Test UNAVAILABLE status (TTL > timeout)
550
+ thread_lock.update('unavailable-key', value='1', timeout=200)
551
+ assert thread_lock.key_status('unavailable-key', timeout=100) == LockStatus.UNAVAILABLE
552
+
553
+ def test_rlock(self, thread_lock: ThreadLock):
554
+ """Test the rlock method."""
555
+ # First test acquiring a lock
556
+ assert thread_lock.rlock('test-key', value='1') is True
557
+ assert thread_lock.lock_value('test-key') == '1'
558
+
559
+ # Test acquiring the same lock with the same value
560
+ assert thread_lock.rlock('test-key', value='1') is True
561
+
562
+ # Test acquiring the same lock with a different value
563
+ assert thread_lock.rlock('test-key', value='2') is False
564
+
565
+ # Verify the original value wasn't changed
566
+ assert thread_lock.lock_value('test-key') == '1'
567
+
568
+ def test_conditional_setdel_operations(self, thread_lock: ThreadLock):
569
+ """Test the conditional set/del operations."""
570
+ # Set initial value
571
+ test_key = 'cond-key'
572
+ thread_lock.update(test_key, value='10')
573
+
574
+ # Test setgt (greater than)
575
+ assert thread_lock.setgt(key=test_key, value=15) # 15 > 10, should succeed
576
+ assert thread_lock.lock_value(test_key) == '15' # Value should be updated to 15
577
+
578
+ # Test setgt with unsuccessful condition
579
+ assert not thread_lock.setgt(key=test_key, value=5) # 5 < 15, should not succeed
580
+ assert thread_lock.lock_value(test_key) == '15' # Value should remain 15
581
+
582
+ # Test setlt (less than)
583
+ assert thread_lock.setlt(key=test_key, value=5) # 5 < 15, should succeed
584
+ assert thread_lock.lock_value(test_key) == '5' # Value should be updated to 5
585
+
586
+ # Test setlt with unsuccessful condition
587
+ assert not thread_lock.setlt(key=test_key, value=10) # 10 > 5, should not succeed
588
+ assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
589
+
590
+ # Test setge (greater than or equal)
591
+ assert thread_lock.setge(key=test_key, value=5) # 5 >= 5, should succeed
592
+ assert thread_lock.lock_value(test_key) == '5' # Value should still be 5
593
+
594
+ # Test setge with unsuccessful condition
595
+ assert not thread_lock.setge(key=test_key, value=4) # 4 < 5, should not succeed
596
+ assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
597
+
598
+ # Test setle (less than or equal)
599
+ assert thread_lock.setle(key=test_key, value=5) # 5 <= 5, should succeed
600
+ assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
601
+
602
+ # Test setle with unsuccessful condition
603
+ assert not thread_lock.setle(key=test_key, value=6) # 6 > 5, should not succeed
604
+ assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
605
+
606
+ # Test seteq (equal)
607
+ assert thread_lock.seteq(key=test_key, value=5) # 5 == 5, should succeed
608
+ assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
609
+
610
+ # Test seteq with unsuccessful condition
611
+ assert not thread_lock.seteq(key=test_key, value=6) # 6 != 5, should not succeed
612
+ assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
613
+
614
+ # Test setne (not equal)
615
+ assert thread_lock.setne(key=test_key, value=10) # 10 != 5, should succeed
616
+ assert thread_lock.lock_value(test_key) == '10' # Value should be updated to 10
617
+
618
+ # Test setne with unsuccessful condition
619
+ assert not thread_lock.setne(key=test_key, value=10) # 10 == 10, should not succeed
620
+ assert thread_lock.lock_value(test_key) == '10' # Value should remain 10
621
+
622
+ # Test delgt (delete if greater than)
623
+ thread_lock.update(test_key, value='10')
624
+ assert thread_lock.delgt(key=test_key, value=15) # 15 > 10, should delete
625
+ assert not thread_lock.is_locked(test_key) # Key should be deleted
626
+
627
+ # Test dellt (delete if less than)
628
+ thread_lock.update(test_key, value='10')
629
+ assert thread_lock.dellt(key=test_key, value=5) # 5 < 10, should delete
630
+ assert not thread_lock.is_locked(test_key) # Key should be deleted
631
+
632
+ # Test delge (delete if greater than or equal)
633
+ thread_lock.update(test_key, value='10')
634
+ assert thread_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
635
+ assert not thread_lock.is_locked(test_key) # Key should be deleted
636
+
637
+ # Test delle (delete if less than or equal)
638
+ thread_lock.update(test_key, value='10')
639
+ assert thread_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
640
+ assert not thread_lock.is_locked(test_key) # Key should be deleted
641
+
642
+ # Test deleq (delete if equal)
643
+ thread_lock.update(test_key, value='10')
644
+ assert thread_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
645
+ assert not thread_lock.is_locked(test_key) # Key should be deleted
646
+
647
+ # Test delne (delete if not equal)
648
+ thread_lock.update(test_key, value='10')
649
+ assert thread_lock.delne(key=test_key, value=5) # 5 != 10, should delete
650
+ assert not thread_lock.is_locked(test_key) # Key should be deleted
651
+
652
+ def test_thread_safety(self, thread_lock: ThreadLock):
653
+ """Test thread safety of ThreadLock."""
654
+ def worker(key):
655
+ if thread_lock.lock(key):
656
+ time.sleep(0.1) # Simulate some work
657
+ thread_lock.unlock(key)
658
+ return True
659
+ return False
660
+
661
+ # Test multiple threads trying to acquire the same lock
662
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
663
+ futures = [executor.submit(worker, 'test-key') for _ in range(10)]
664
+ results = [f.result() for f in futures]
665
+
666
+ # Only one thread should have successfully acquired the lock
667
+ assert sum(results) == 1
668
+
669
+ def test_invalid_operator(self, thread_lock: ThreadLock):
670
+ """Test that an invalid operator raises a ValueError."""
671
+ with pytest.raises(ValueError, match="Invalid operator"):
672
+ thread_lock._compare_values("invalid", 10.0, 5.0)
673
+
674
+ def test_timeout_types(self, thread_lock: ThreadLock):
675
+ """Test different timeout types."""
676
+ from datetime import timedelta, datetime
677
+
678
+ # Test with timedelta
679
+ test_key = 'timeout-key'
680
+
681
+ # Use timedelta for timeout (30 seconds)
682
+ thread_lock.update(test_key, value='1', timeout=timedelta(seconds=30))
683
+ assert thread_lock.is_locked(test_key)
684
+ assert thread_lock.key_status(test_key) == LockStatus.LOCKED
685
+
686
+ # Use None for timeout (should use a very long timeout)
687
+ thread_lock.update(test_key, value='2', timeout=None)
688
+ assert thread_lock.is_locked(test_key)
689
+ # The TTL should be very high (set to 2099)
690
+ future_time = datetime(2099, 1, 1).timestamp() - time.time()
691
+ # Allow 10 seconds margin in the test
692
+ assert thread_lock._get_ttl(test_key) > future_time - 10
693
+
694
+
695
+ class TestThreadLockPool:
696
+ """Tests for the ThreadLockPool class."""
697
+
698
+ def test_keys(self, thread_lock_pool: ThreadLockPool):
699
+ """Test the keys method."""
700
+ # Add some keys to the pool
701
+ thread_lock_pool.assign(['key1', 'key2'])
702
+ keys = thread_lock_pool.keys()
703
+ assert sorted(list(keys)) == ['key1', 'key2']
704
+
705
+ def test_extend(self, thread_lock_pool: ThreadLockPool):
706
+ """Test the extend method with keys."""
707
+ thread_lock_pool.extend(['key1', 'key2'])
708
+
709
+ # Verify keys were added
710
+ assert 'key1' in thread_lock_pool
711
+ assert 'key2' in thread_lock_pool
712
+
713
+ def test_shrink(self, thread_lock_pool: ThreadLockPool):
714
+ """Test the shrink method."""
715
+ # First add keys
716
+ thread_lock_pool.extend(['key1', 'key2', 'key3'])
717
+
718
+ # Then shrink
719
+ thread_lock_pool.shrink(['key1', 'key2'])
720
+
721
+ # Verify keys were removed
722
+ assert 'key1' not in thread_lock_pool
723
+ assert 'key2' not in thread_lock_pool
724
+ assert 'key3' in thread_lock_pool
725
+
726
+ def test_clear(self, thread_lock_pool: ThreadLockPool):
727
+ """Test the clear method."""
728
+ # First add keys
729
+ thread_lock_pool.extend(['key1', 'key2'])
730
+
731
+ # Then clear
732
+ thread_lock_pool.clear()
733
+
734
+ # Verify all keys were removed
735
+ assert len(thread_lock_pool) == 0
736
+
737
+ def test_assign(self, thread_lock_pool: ThreadLockPool):
738
+ """Test the assign method."""
739
+ # First add some initial keys
740
+ thread_lock_pool.extend(['old1', 'old2'])
741
+
742
+ # Then assign new keys
743
+ thread_lock_pool.assign(['key1', 'key2'])
744
+
745
+ # Verify old keys were removed and new keys were added
746
+ assert 'old1' not in thread_lock_pool
747
+ assert 'old2' not in thread_lock_pool
748
+ assert 'key1' in thread_lock_pool
749
+ assert 'key2' in thread_lock_pool
750
+
751
+ def test_contains(self, thread_lock_pool: ThreadLockPool):
752
+ """Test the __contains__ method."""
753
+ # Add a key
754
+ thread_lock_pool.extend(['key1'])
755
+
756
+ # Test __contains__
757
+ assert 'key1' in thread_lock_pool
758
+ assert 'key2' not in thread_lock_pool
759
+
760
+ def test_set_del_item(self, thread_lock_pool: ThreadLockPool):
761
+ """Test the __setitem__ and __delitem__ methods."""
762
+ # First test extend to add a key
763
+ thread_lock_pool.extend(['key1'])
764
+
765
+ # Test __getitem__
766
+ assert 'key1' in thread_lock_pool
767
+
768
+ # Test shrink to remove key
769
+ thread_lock_pool.shrink(['key1'])
770
+ assert 'key1' not in thread_lock_pool
771
+
772
+ def test_health_check(self, thread_lock_pool: ThreadLockPool):
773
+ """Test the health_check method."""
774
+ # Add some keys
775
+ thread_lock_pool.extend(['key1', 'key2', 'key3'])
776
+
777
+ # Lock some keys
778
+ thread_lock_pool.lock('key1')
779
+ thread_lock_pool.lock('key2')
780
+
781
+ # Check health
782
+ locked, free = thread_lock_pool.health_check()
783
+ assert locked == 2
784
+ assert free == 1
785
+
786
+ def test_thread_safety(self, thread_lock_pool: ThreadLockPool):
787
+ """Test thread safety of ThreadLockPool."""
788
+ def worker(key):
789
+ if thread_lock_pool.lock(key):
790
+ time.sleep(0.1) # Simulate some work
791
+ thread_lock_pool.unlock(key)
792
+ return True
793
+ return False
794
+
795
+ # Add keys to the pool
796
+ thread_lock_pool.extend(['test-key'])
797
+
798
+ # Test multiple threads trying to acquire the same lock
799
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
800
+ futures = [executor.submit(worker, 'test-key') for _ in range(10)]
801
+ results = [f.result() for f in futures]
802
+
803
+ # Only one thread should have successfully acquired the lock
804
+ assert sum(results) == 1
805
+
806
+ def test_empty_health_check(self, thread_lock_pool: ThreadLockPool):
807
+ """Test health_check on an empty pool."""
808
+ # Clear the pool first
809
+ thread_lock_pool.clear()
810
+
811
+ # Check health of empty pool
812
+ locked, free = thread_lock_pool.health_check()
813
+ assert locked == 0
814
+ assert free == 0
815
+
816
+ def test_empty_pool_operations(self, thread_lock_pool: ThreadLockPool):
817
+ """Test operations on an empty pool."""
818
+ # Test clear on already empty pool
819
+ thread_lock_pool.clear()
820
+ assert len(thread_lock_pool) == 0
821
+
822
+ # Test shrink on empty pool
823
+ thread_lock_pool.shrink(['nonexistent'])
824
+ assert len(thread_lock_pool) == 0
825
+
826
+ # Test extend with None
827
+ thread_lock_pool.extend(None)
828
+ assert len(thread_lock_pool) == 0
829
+
830
+ # Test assign with None
831
+ thread_lock_pool.assign(None)
832
+ assert len(thread_lock_pool) == 0
833
+
834
+ def test_iterate_pool(self, thread_lock_pool: ThreadLockPool):
835
+ """Test iterating over the keys in the pool."""
836
+ # Add some keys
837
+ test_keys = ['key1', 'key2', 'key3']
838
+ thread_lock_pool.extend(test_keys)
839
+
840
+ # Iterate and collect keys
841
+ iterated_keys = []
842
+ for key in thread_lock_pool:
843
+ iterated_keys.append(key)
844
+
845
+ # Verify all keys were iterated
846
+ assert sorted(iterated_keys) == sorted(test_keys)
847
+
848
+ def test_len_empty_pool(self, thread_lock_pool: ThreadLockPool):
849
+ """Test len() on empty pool."""
850
+ thread_lock_pool.clear()
851
+ assert len(thread_lock_pool) == 0