Skip to content

Commit b6350a1

Browse files
authored
Generalize stateful store test (#2202)
* Generalize stateful store test * Fix for localstore * cleanup * Address feedback * Limit binary size, and range for key_ranges * small cleanup
1 parent 1560d21 commit b6350a1

File tree

3 files changed

+60
-24
lines changed

3 files changed

+60
-24
lines changed

src/zarr/testing/strategies.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ def v2_dtypes() -> st.SearchStrategy[np.dtype]:
6060
)
6161
array_names = node_names
6262
attrs = st.none() | st.dictionaries(_attr_keys, _attr_values)
63-
paths = st.lists(node_names, min_size=1).map("/".join) | st.just("/")
63+
keys = st.lists(node_names, min_size=1).map(lambda x: "/".join(x))
64+
paths = st.just("/") | keys
6465
stores = st.builds(MemoryStore, st.just({}), mode=st.just("w"))
6566
compressors = st.sampled_from([None, "default"])
6667
zarr_formats: st.SearchStrategy[Literal[2, 3]] = st.sampled_from([2, 3])
@@ -171,7 +172,9 @@ def basic_indices(draw: st.DrawFn, *, shape: tuple[int], **kwargs): # type: ign
171172
)
172173

173174

174-
def key_ranges(keys: SearchStrategy = node_names) -> SearchStrategy[list]:
175+
def key_ranges(
176+
keys: SearchStrategy = node_names, max_size: int | None = None
177+
) -> SearchStrategy[list[int]]:
175178
"""
176179
Function to generate key_ranges strategy for get_partial_values()
177180
returns list strategy w/ form::
@@ -180,7 +183,8 @@ def key_ranges(keys: SearchStrategy = node_names) -> SearchStrategy[list]:
180183
(key, (range_start, range_step)),...]
181184
"""
182185
byte_ranges = st.tuples(
183-
st.none() | st.integers(min_value=0), st.none() | st.integers(min_value=0)
186+
st.none() | st.integers(min_value=0, max_value=max_size),
187+
st.none() | st.integers(min_value=0, max_value=max_size),
184188
)
185189
key_tuple = st.tuples(keys, byte_ranges)
186190
return st.lists(key_tuple, min_size=1, max_size=10)

tests/v3/conftest.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from hypothesis import HealthCheck, Verbosity, settings
1111

1212
from zarr import AsyncGroup, config
13+
from zarr.abc.store import Store
14+
from zarr.core.sync import sync
1315
from zarr.store import LocalStore, MemoryStore, StorePath, ZipStore
1416
from zarr.store.remote import RemoteStore
1517

@@ -19,7 +21,6 @@
1921

2022
from _pytest.compat import LEGACY_PATH
2123

22-
from zarr.abc.store import Store
2324
from zarr.core.common import ChunkCoords, MemoryOrder, ZarrFormat
2425

2526

@@ -75,6 +76,14 @@ async def store(request: pytest.FixtureRequest, tmpdir: LEGACY_PATH) -> Store:
7576
return await parse_store(param, str(tmpdir))
7677

7778

79+
@pytest.fixture(params=["local", "memory", "zip"])
80+
def sync_store(request: pytest.FixtureRequest, tmp_path: LEGACY_PATH) -> Store:
81+
result = sync(parse_store(request.param, str(tmp_path)))
82+
if not isinstance(result, Store):
83+
raise TypeError("Wrong store class returned by test fixture! got " + result + " instead")
84+
return result
85+
86+
7887
@dataclass
7988
class AsyncGroupRequest:
8089
zarr_format: ZarrFormat

tests/v3/test_store/test_stateful_store.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
# Stateful tests for arbitrary Zarr stores.
2-
3-
42
import hypothesis.strategies as st
3+
import pytest
54
from hypothesis import assume, note
65
from hypothesis.stateful import (
76
RuleBasedStateMachine,
7+
Settings,
8+
initialize,
89
invariant,
910
precondition,
1011
rule,
12+
run_state_machine_as_test,
1113
)
1214
from hypothesis.strategies import DataObject
1315

1416
import zarr
1517
from zarr.abc.store import AccessMode, Store
1618
from 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

2126
class 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

Comments
 (0)