@@ -111,7 +111,9 @@ def test_sharding_scalar(
111111 indirect = ["array_fixture" ],
112112)
113113def test_sharding_partial (
114- store : Store , array_fixture : npt .NDArray [Any ], index_location : ShardingCodecIndexLocation
114+ store : Store ,
115+ array_fixture : npt .NDArray [Any ],
116+ index_location : ShardingCodecIndexLocation ,
115117) -> None :
116118 data = array_fixture
117119 spath = StorePath (store )
@@ -147,7 +149,9 @@ def test_sharding_partial(
147149 indirect = ["array_fixture" ],
148150)
149151def test_sharding_partial_readwrite (
150- store : Store , array_fixture : npt .NDArray [Any ], index_location : ShardingCodecIndexLocation
152+ store : Store ,
153+ array_fixture : npt .NDArray [Any ],
154+ index_location : ShardingCodecIndexLocation ,
151155) -> None :
152156 data = array_fixture
153157 spath = StorePath (store )
@@ -179,7 +183,9 @@ def test_sharding_partial_readwrite(
179183@pytest .mark .parametrize ("index_location" , ["start" , "end" ])
180184@pytest .mark .parametrize ("store" , ["local" , "memory" , "zip" ], indirect = ["store" ])
181185def test_sharding_partial_read (
182- store : Store , array_fixture : npt .NDArray [Any ], index_location : ShardingCodecIndexLocation
186+ store : Store ,
187+ array_fixture : npt .NDArray [Any ],
188+ index_location : ShardingCodecIndexLocation ,
183189) -> None :
184190 data = array_fixture
185191 spath = StorePath (store )
@@ -338,6 +344,114 @@ def test_sharding_multiple_chunks_partial_shard_read(
338344 assert isinstance (kwargs ["byte_range" ], (SuffixByteRequest , RangeByteRequest ))
339345
340346
347+ @pytest .mark .parametrize ("index_location" , ["start" , "end" ])
348+ @pytest .mark .parametrize ("store" , ["local" , "memory" , "zip" ], indirect = ["store" ])
349+ def test_sharding_partial_shard_read__index_load_fails (
350+ store : Store , index_location : ShardingCodecIndexLocation
351+ ) -> None :
352+ """Test fill value is returned when the call to the store to load the bytes of the shard's chunk index fails."""
353+ array_shape = (16 ,)
354+ shard_shape = (16 ,)
355+ chunk_shape = (8 ,)
356+ data = np .arange (np .prod (array_shape ), dtype = "float32" ).reshape (array_shape )
357+ fill_value = - 999
358+
359+ store_mock = AsyncMock (wraps = store , spec = store .__class__ )
360+ # loading the index is the first call to .get() so returning None will simulate an index load failure
361+ store_mock .get .return_value = None
362+
363+ a = zarr .create_array (
364+ StorePath (store_mock ),
365+ shape = data .shape ,
366+ chunks = chunk_shape ,
367+ shards = {"shape" : shard_shape , "index_location" : index_location },
368+ compressors = BloscCodec (cname = "lz4" ),
369+ dtype = data .dtype ,
370+ fill_value = fill_value ,
371+ )
372+ a [:] = data
373+
374+ # Read from one of two chunks in a shard to test the partial shard read path
375+ assert a [0 ] == fill_value
376+ assert a [0 ] != data [0 ]
377+
378+
379+ @pytest .mark .parametrize ("index_location" , ["start" , "end" ])
380+ @pytest .mark .parametrize ("store" , ["local" , "memory" , "zip" ], indirect = ["store" ])
381+ def test_sharding_partial_shard_read__index_chunk_slice_fails (
382+ store : Store ,
383+ index_location : ShardingCodecIndexLocation ,
384+ monkeypatch : pytest .MonkeyPatch ,
385+ ) -> None :
386+ """Test fill value is returned when looking up a chunk's byte slice within a shard fails."""
387+ array_shape = (16 ,)
388+ shard_shape = (16 ,)
389+ chunk_shape = (8 ,)
390+ data = np .arange (np .prod (array_shape ), dtype = "float32" ).reshape (array_shape )
391+ fill_value = - 999
392+
393+ monkeypatch .setattr (
394+ "zarr.codecs.sharding._ShardIndex.get_chunk_slice" ,
395+ lambda self , chunk_coords : None ,
396+ )
397+
398+ a = zarr .create_array (
399+ StorePath (store ),
400+ shape = data .shape ,
401+ chunks = chunk_shape ,
402+ shards = {"shape" : shard_shape , "index_location" : index_location },
403+ compressors = BloscCodec (cname = "lz4" ),
404+ dtype = data .dtype ,
405+ fill_value = fill_value ,
406+ )
407+ a [:] = data
408+
409+ # Read from one of two chunks in a shard to test the partial shard read path
410+ assert a [0 ] == fill_value
411+ assert a [0 ] != data [0 ]
412+
413+
414+ @pytest .mark .parametrize ("index_location" , ["start" , "end" ])
415+ @pytest .mark .parametrize ("store" , ["local" , "memory" , "zip" ], indirect = ["store" ])
416+ def test_sharding_partial_shard_read__chunk_load_fails (
417+ store : Store , index_location : ShardingCodecIndexLocation
418+ ) -> None :
419+ """Test fill value is returned when the call to the store to load a chunk's bytes fails."""
420+ array_shape = (16 ,)
421+ shard_shape = (16 ,)
422+ chunk_shape = (8 ,)
423+ data = np .arange (np .prod (array_shape ), dtype = "float32" ).reshape (array_shape )
424+ fill_value = - 999
425+
426+ store_mock = AsyncMock (wraps = store , spec = store .__class__ )
427+
428+ a = zarr .create_array (
429+ StorePath (store_mock ),
430+ shape = data .shape ,
431+ chunks = chunk_shape ,
432+ shards = {"shape" : shard_shape , "index_location" : index_location },
433+ compressors = BloscCodec (cname = "lz4" ),
434+ dtype = data .dtype ,
435+ fill_value = fill_value ,
436+ )
437+ a [:] = data
438+
439+ # Set up store mock after array creation to only modify calls during array indexing
440+ # Succeed on first call (index load), fail on subsequent calls (chunk loads)
441+ async def first_success_then_fail (* args : Any , ** kwargs : Any ) -> Any :
442+ if store_mock .get .call_count == 1 :
443+ return await store .get (* args , ** kwargs )
444+ else :
445+ return None
446+
447+ store_mock .get .reset_mock ()
448+ store_mock .get .side_effect = first_success_then_fail
449+
450+ # Read from one of two chunks in a shard to test the partial shard read path
451+ assert a [0 ] == fill_value
452+ assert a [0 ] != data [0 ]
453+
454+
341455@pytest .mark .parametrize (
342456 "array_fixture" ,
343457 [
@@ -348,7 +462,9 @@ def test_sharding_multiple_chunks_partial_shard_read(
348462@pytest .mark .parametrize ("index_location" , ["start" , "end" ])
349463@pytest .mark .parametrize ("store" , ["local" , "memory" , "zip" ], indirect = ["store" ])
350464def test_sharding_partial_overwrite (
351- store : Store , array_fixture : npt .NDArray [Any ], index_location : ShardingCodecIndexLocation
465+ store : Store ,
466+ array_fixture : npt .NDArray [Any ],
467+ index_location : ShardingCodecIndexLocation ,
352468) -> None :
353469 data = array_fixture [:10 , :10 , :10 ]
354470 spath = StorePath (store )
@@ -578,7 +694,9 @@ async def test_sharding_with_empty_inner_chunk(
578694)
579695@pytest .mark .parametrize ("chunks_per_shard" , [(5 , 2 ), (2 , 5 ), (5 , 5 )])
580696async def test_sharding_with_chunks_per_shard (
581- store : Store , index_location : ShardingCodecIndexLocation , chunks_per_shard : tuple [int ]
697+ store : Store ,
698+ index_location : ShardingCodecIndexLocation ,
699+ chunks_per_shard : tuple [int ],
582700) -> None :
583701 chunk_shape = (2 , 1 )
584702 shape = tuple (x * y for x , y in zip (chunks_per_shard , chunk_shape , strict = False ))
0 commit comments