11# Stateful tests for arbitrary Zarr stores.
2-
3-
42import hypothesis .strategies as st
3+ import pytest
54from hypothesis import assume , note
65from hypothesis .stateful import (
76 RuleBasedStateMachine ,
7+ Settings ,
8+ initialize ,
89 invariant ,
910 precondition ,
1011 rule ,
12+ run_state_machine_as_test ,
1113)
1214from hypothesis .strategies import DataObject
1315
1416import zarr
1517from zarr .abc .store import AccessMode , Store
1618from zarr .core .buffer import BufferPrototype , cpu , default_buffer_prototype
17- from zarr .store import MemoryStore
18- from zarr .testing .strategies import key_ranges , paths
19+ from zarr .store import LocalStore , ZipStore
20+ from zarr .testing .strategies import key_ranges
21+ from zarr .testing .strategies import keys as zarr_keys
22+
23+ MAX_BINARY_SIZE = 100
1924
2025
2126class SyncStoreWrapper (zarr .core .sync .SyncMixin ):
@@ -99,13 +104,17 @@ class ZarrStoreStateMachine(RuleBasedStateMachine):
99104 https://hypothesis.readthedocs.io/en/latest/stateful.html
100105 """
101106
102- def __init__ (self ) -> None :
107+ def __init__ (self , store : Store ) -> None :
103108 super ().__init__ ()
104109 self .model : dict [str , bytes ] = {}
105- self .store = SyncStoreWrapper (MemoryStore ( mode = "w" ) )
110+ self .store = SyncStoreWrapper (store )
106111 self .prototype = default_buffer_prototype ()
107112
108- @rule (key = paths , data = st .binary (min_size = 0 , max_size = 100 ))
113+ @initialize ()
114+ def init_store (self ):
115+ self .store .clear ()
116+
117+ @rule (key = zarr_keys , data = st .binary (min_size = 0 , max_size = MAX_BINARY_SIZE ))
109118 def set (self , key : str , data : DataObject ) -> None :
110119 note (f"(set) Setting { key !r} with { data } " )
111120 assert not self .store .mode .readonly
@@ -114,7 +123,7 @@ def set(self, key: str, data: DataObject) -> None:
114123 self .model [key ] = data_buf
115124
116125 @precondition (lambda self : len (self .model .keys ()) > 0 )
117- @rule (key = paths , data = st .data ())
126+ @rule (key = zarr_keys , data = st .data ())
118127 def get (self , key : str , data : DataObject ) -> None :
119128 key = data .draw (
120129 st .sampled_from (sorted (self .model .keys ()))
@@ -124,16 +133,18 @@ def get(self, key: str, data: DataObject) -> None:
124133 # to bytes here necessary because data_buf set to model in set()
125134 assert self .model [key ].to_bytes () == (store_value .to_bytes ())
126135
127- @rule (key = paths , data = st .data ())
128- def get_invalid_keys (self , key : str , data : DataObject ) -> None :
136+ @rule (key = zarr_keys , data = st .data ())
137+ def get_invalid_zarr_keys (self , key : str , data : DataObject ) -> None :
129138 note ("(get_invalid)" )
130139 assume (key not in self .model )
131140 assert self .store .get (key , self .prototype ) is None
132141
133142 @precondition (lambda self : len (self .model .keys ()) > 0 )
134143 @rule (data = st .data ())
135144 def get_partial_values (self , data : DataObject ) -> None :
136- key_range = data .draw (key_ranges (keys = st .sampled_from (sorted (self .model .keys ()))))
145+ key_range = data .draw (
146+ key_ranges (keys = st .sampled_from (sorted (self .model .keys ())), max_size = MAX_BINARY_SIZE )
147+ )
137148 note (f"(get partial) { key_range = } " )
138149 obs_maybe = self .store .get_partial_values (key_range , self .prototype )
139150 observed = []
@@ -173,16 +184,20 @@ def clear(self) -> None:
173184 self .store .clear ()
174185 self .model .clear ()
175186
187+ assert self .store .empty ()
188+
176189 assert len (self .model .keys ()) == len (list (self .store .list ())) == 0
177190
178191 @rule ()
192+ # Local store can be non-empty when there are subdirectories but no files
193+ @precondition (lambda self : not isinstance (self .store .store , LocalStore ))
179194 def empty (self ) -> None :
180195 note ("(empty)" )
181196
182197 # make sure they either both are or both aren't empty (same state)
183198 assert self .store .empty () == (not self .model )
184199
185- @rule (key = paths )
200+ @rule (key = zarr_keys )
186201 def exists (self , key : str ) -> None :
187202 note ("(exists)" )
188203
@@ -191,9 +206,9 @@ def exists(self, key: str) -> None:
191206 @invariant ()
192207 def check_paths_equal (self ) -> None :
193208 note ("Checking that paths are equal" )
194- paths = list (self .store .list ())
209+ paths = sorted (self .store .list ())
195210
196- assert list (self .model .keys ()) == paths
211+ assert sorted (self .model .keys ()) == paths
197212
198213 @invariant ()
199214 def check_vals_equal (self ) -> None :
@@ -203,24 +218,32 @@ def check_vals_equal(self) -> None:
203218 assert val .to_bytes () == store_item
204219
205220 @invariant ()
206- def check_num_keys_equal (self ) -> None :
207- note ("check num keys equal" )
221+ def check_num_zarr_keys_equal (self ) -> None :
222+ note ("check num zarr_keys equal" )
208223
209224 assert len (self .model ) == len (list (self .store .list ()))
210225
211226 @invariant ()
212- def check_keys (self ) -> None :
227+ def check_zarr_keys (self ) -> None :
213228 keys = list (self .store .list ())
214229
215- if len ( keys ) == 0 :
230+ if not keys :
216231 assert self .store .empty () is True
217232
218- elif len ( keys ) != 0 :
233+ else :
219234 assert self .store .empty () is False
220235
221236 for key in keys :
222237 assert self .store .exists (key ) is True
223238 note ("checking keys / exists / empty" )
224239
225240
226- StatefulStoreTest = ZarrStoreStateMachine .TestCase
241+ def test_zarr_hierarchy (sync_store : Store ) -> None :
242+ def mk_test_instance_sync ():
243+ return ZarrStoreStateMachine (sync_store )
244+
245+ if isinstance (sync_store , ZipStore ):
246+ pytest .skip (reason = "ZipStore does not support delete" )
247+ if isinstance (sync_store , LocalStore ):
248+ pytest .skip (reason = "This test has errors" )
249+ run_state_machine_as_test (mk_test_instance_sync , settings = Settings (report_multiple_bugs = True ))
0 commit comments