@@ -299,3 +299,295 @@ async def test_clear_cache(self, cached_store: CacheStore) -> None:
299
299
# Verify data still exists in source store
300
300
assert await cached_store ._store .exists ("clear_test_1" )
301
301
assert await cached_store ._store .exists ("clear_test_2" )
302
+
303
+ async def test_max_age_infinity (self ) -> None :
304
+ """Test cache with infinite max age."""
305
+ source_store = MemoryStore ()
306
+ cache_store = MemoryStore ()
307
+ cached_store = CacheStore (
308
+ source_store ,
309
+ cache_store = cache_store ,
310
+ max_age_seconds = "infinity"
311
+ )
312
+
313
+ # Add data and verify it never expires
314
+ test_data = CPUBuffer .from_bytes (b"test data" )
315
+ await cached_store .set ("test_key" , test_data )
316
+
317
+ # Even after time passes, key should be fresh
318
+ assert cached_store ._is_key_fresh ("test_key" )
319
+
320
+ async def test_max_age_numeric (self ) -> None :
321
+ """Test cache with numeric max age."""
322
+ source_store = MemoryStore ()
323
+ cache_store = MemoryStore ()
324
+ cached_store = CacheStore (
325
+ source_store ,
326
+ cache_store = cache_store ,
327
+ max_age_seconds = 1 # 1 second
328
+ )
329
+
330
+ # Add data
331
+ test_data = CPUBuffer .from_bytes (b"test data" )
332
+ await cached_store .set ("test_key" , test_data )
333
+
334
+ # Key should be fresh initially
335
+ assert cached_store ._is_key_fresh ("test_key" )
336
+
337
+ # Manually set old timestamp to test expiration
338
+ cached_store .key_insert_times ["test_key" ] = time .monotonic () - 2 # 2 seconds ago
339
+
340
+ # Key should now be stale
341
+ assert not cached_store ._is_key_fresh ("test_key" )
342
+
343
+ async def test_cache_set_data_disabled (self ) -> None :
344
+ """Test cache behavior when cache_set_data is False."""
345
+ source_store = MemoryStore ()
346
+ cache_store = MemoryStore ()
347
+ cached_store = CacheStore (
348
+ source_store ,
349
+ cache_store = cache_store ,
350
+ cache_set_data = False
351
+ )
352
+
353
+ # Set data
354
+ test_data = CPUBuffer .from_bytes (b"test data" )
355
+ await cached_store .set ("test_key" , test_data )
356
+
357
+ # Data should be in source but not in cache
358
+ assert await source_store .exists ("test_key" )
359
+ assert not await cache_store .exists ("test_key" )
360
+
361
+ # Cache info should show no cached data
362
+ info = cached_store .cache_info ()
363
+ assert info ["cache_set_data" ] is False
364
+ assert info ["cached_keys" ] == 0
365
+
366
+ async def test_eviction_with_max_size (self ) -> None :
367
+ """Test LRU eviction when max_size is exceeded."""
368
+ source_store = MemoryStore ()
369
+ cache_store = MemoryStore ()
370
+ cached_store = CacheStore (
371
+ source_store ,
372
+ cache_store = cache_store ,
373
+ max_size = 100 # Small cache size
374
+ )
375
+
376
+ # Add data that exceeds cache size
377
+ small_data = CPUBuffer .from_bytes (b"a" * 40 ) # 40 bytes
378
+ medium_data = CPUBuffer .from_bytes (b"b" * 40 ) # 40 bytes
379
+ large_data = CPUBuffer .from_bytes (b"c" * 40 ) # 40 bytes (would exceed 100 byte limit)
380
+
381
+ # Set first two items
382
+ await cached_store .set ("key1" , small_data )
383
+ await cached_store .set ("key2" , medium_data )
384
+
385
+ # Cache should have 2 items
386
+ info = cached_store .cache_info ()
387
+ assert info ["cached_keys" ] == 2
388
+ assert info ["current_size" ] == 80
389
+
390
+ # Add third item - should trigger eviction of first item
391
+ await cached_store .set ("key3" , large_data )
392
+
393
+ # Cache should still have items but first one may be evicted
394
+ info = cached_store .cache_info ()
395
+ assert info ["current_size" ] <= 100
396
+
397
+ async def test_value_exceeds_max_size (self ) -> None :
398
+ """Test behavior when a single value exceeds max_size."""
399
+ source_store = MemoryStore ()
400
+ cache_store = MemoryStore ()
401
+ cached_store = CacheStore (
402
+ source_store ,
403
+ cache_store = cache_store ,
404
+ max_size = 50 # Small cache size
405
+ )
406
+
407
+ # Try to cache data larger than max_size
408
+ large_data = CPUBuffer .from_bytes (b"x" * 100 ) # 100 bytes > 50 byte limit
409
+ await cached_store .set ("large_key" , large_data )
410
+
411
+ # Data should be in source but not cached
412
+ assert await source_store .exists ("large_key" )
413
+ info = cached_store .cache_info ()
414
+ assert info ["cached_keys" ] == 0
415
+ assert info ["current_size" ] == 0
416
+
417
+ async def test_get_nonexistent_key (self ) -> None :
418
+ """Test getting a key that doesn't exist in either store."""
419
+ source_store = MemoryStore ()
420
+ cache_store = MemoryStore ()
421
+ cached_store = CacheStore (source_store , cache_store = cache_store )
422
+
423
+ # Try to get nonexistent key
424
+ result = await cached_store .get ("nonexistent" , default_buffer_prototype ())
425
+ assert result is None
426
+
427
+ # Should not create any cache entries
428
+ info = cached_store .cache_info ()
429
+ assert info ["cached_keys" ] == 0
430
+
431
+ async def test_delete_both_stores (self ) -> None :
432
+ """Test that delete removes from both source and cache stores."""
433
+ source_store = MemoryStore ()
434
+ cache_store = MemoryStore ()
435
+ cached_store = CacheStore (source_store , cache_store = cache_store )
436
+
437
+ # Add data
438
+ test_data = CPUBuffer .from_bytes (b"test data" )
439
+ await cached_store .set ("test_key" , test_data )
440
+
441
+ # Verify it's in both stores
442
+ assert await source_store .exists ("test_key" )
443
+ assert await cache_store .exists ("test_key" )
444
+
445
+ # Delete
446
+ await cached_store .delete ("test_key" )
447
+
448
+ # Verify it's removed from both
449
+ assert not await source_store .exists ("test_key" )
450
+ assert not await cache_store .exists ("test_key" )
451
+
452
+ # Verify tracking is updated
453
+ info = cached_store .cache_info ()
454
+ assert info ["cached_keys" ] == 0
455
+
456
+ async def test_invalid_max_age_seconds (self ) -> None :
457
+ """Test that invalid max_age_seconds values raise ValueError."""
458
+ source_store = MemoryStore ()
459
+ cache_store = MemoryStore ()
460
+
461
+ with pytest .raises (ValueError , match = "max_age_seconds string value must be 'infinity'" ):
462
+ CacheStore (
463
+ source_store ,
464
+ cache_store = cache_store ,
465
+ max_age_seconds = "invalid"
466
+ )
467
+
468
+ async def test_buffer_size_function_coverage (self ) -> None :
469
+ """Test different branches of the buffer_size function."""
470
+ from zarr .storage ._caching_store import buffer_size
471
+
472
+ # Test with Buffer object (nbytes attribute)
473
+ buffer_data = CPUBuffer .from_bytes (b"test data" )
474
+ size = buffer_size (buffer_data )
475
+ assert size > 0
476
+
477
+ # Test with bytes
478
+ bytes_data = b"test bytes"
479
+ size = buffer_size (bytes_data )
480
+ assert size == len (bytes_data )
481
+
482
+ # Test with bytearray
483
+ bytearray_data = bytearray (b"test bytearray" )
484
+ size = buffer_size (bytearray_data )
485
+ assert size == len (bytearray_data )
486
+
487
+ # Test with memoryview
488
+ memoryview_data = memoryview (b"test memoryview" )
489
+ size = buffer_size (memoryview_data )
490
+ assert size == len (memoryview_data )
491
+
492
+ # Test fallback for other types - use a simple object
493
+ # This will go through the numpy fallback or string encoding
494
+ size = buffer_size ("test string" )
495
+ assert size > 0
496
+
497
+ async def test_unlimited_cache_size (self ) -> None :
498
+ """Test behavior when max_size is None (unlimited)."""
499
+ source_store = MemoryStore ()
500
+ cache_store = MemoryStore ()
501
+ cached_store = CacheStore (
502
+ source_store ,
503
+ cache_store = cache_store ,
504
+ max_size = None # Unlimited cache
505
+ )
506
+
507
+ # Add large amounts of data
508
+ for i in range (10 ):
509
+ large_data = CPUBuffer .from_bytes (b"x" * 1000 ) # 1KB each
510
+ await cached_store .set (f"large_key_{ i } " , large_data )
511
+
512
+ # All should be cached since there's no size limit
513
+ info = cached_store .cache_info ()
514
+ assert info ["cached_keys" ] == 10
515
+ assert info ["current_size" ] == 10000 # 10 * 1000 bytes
516
+
517
+ async def test_evict_key_exception_handling (self ) -> None :
518
+ """Test exception handling in _evict_key method."""
519
+ source_store = MemoryStore ()
520
+ cache_store = MemoryStore ()
521
+ cached_store = CacheStore (
522
+ source_store ,
523
+ cache_store = cache_store ,
524
+ max_size = 100
525
+ )
526
+
527
+ # Add some data
528
+ test_data = CPUBuffer .from_bytes (b"test data" )
529
+ await cached_store .set ("test_key" , test_data )
530
+
531
+ # Manually corrupt the tracking to trigger exception
532
+ # Remove from one structure but not others to create inconsistency
533
+ del cached_store ._cache_order ["test_key" ]
534
+
535
+ # Try to evict - should handle the KeyError gracefully
536
+ cached_store ._evict_key ("test_key" )
537
+
538
+ # Should still work and not crash
539
+ info = cached_store .cache_info ()
540
+ assert isinstance (info , dict )
541
+
542
+ async def test_get_no_cache_delete_tracking (self ) -> None :
543
+ """Test _get_no_cache when key doesn't exist and needs cleanup."""
544
+ source_store = MemoryStore ()
545
+ cache_store = MemoryStore ()
546
+ cached_store = CacheStore (source_store , cache_store = cache_store )
547
+
548
+ # First, add key to cache tracking but not to source
549
+ test_data = CPUBuffer .from_bytes (b"test data" )
550
+ await cache_store .set ("phantom_key" , test_data )
551
+ cached_store ._cache_value ("phantom_key" , test_data )
552
+
553
+ # Verify it's in tracking
554
+ assert "phantom_key" in cached_store ._cache_order
555
+ assert "phantom_key" in cached_store .key_insert_times
556
+
557
+ # Now try to get it - since it's not in source, should clean up tracking
558
+ result = await cached_store ._get_no_cache ("phantom_key" , default_buffer_prototype ())
559
+ assert result is None
560
+
561
+ # Should have cleaned up tracking
562
+ assert "phantom_key" not in cached_store ._cache_order
563
+ assert "phantom_key" not in cached_store .key_insert_times
564
+
565
+ async def test_buffer_size_import_error_fallback (self ) -> None :
566
+ """Test buffer_size ImportError fallback."""
567
+ from unittest .mock import patch
568
+
569
+ from zarr .storage ._caching_store import buffer_size
570
+
571
+ # Mock numpy import to raise ImportError
572
+ with patch .dict ('sys.modules' , {'numpy' : None }):
573
+ with patch ('builtins.__import__' , side_effect = ImportError ("No module named 'numpy'" )):
574
+ # This should trigger the ImportError fallback
575
+ size = buffer_size ("test string" )
576
+ assert size == len (b"test string" )
577
+
578
+ async def test_accommodate_value_no_max_size (self ) -> None :
579
+ """Test _accommodate_value early return when max_size is None."""
580
+ source_store = MemoryStore ()
581
+ cache_store = MemoryStore ()
582
+ cached_store = CacheStore (
583
+ source_store ,
584
+ cache_store = cache_store ,
585
+ max_size = None # No size limit
586
+ )
587
+
588
+ # This should return early without doing anything
589
+ cached_store ._accommodate_value (1000000 ) # Large value
590
+
591
+ # Should not affect anything since max_size is None
592
+ info = cached_store .cache_info ()
593
+ assert info ["current_size" ] == 0
0 commit comments