-
-
Notifications
You must be signed in to change notification settings - Fork 346
Stateful store tests #2070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Stateful store tests #2070
Changes from 39 commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
78cb80c
stateful store test
e-marshall dcf81d6
Deepak's edits
dcherian bb18f75
add data strategies and beginning of testgetpartial
e-marshall ddd669e
a few more updates
e-marshall 6927577
troubleshooting get_partial
e-marshall 6b43dc3
edits
e-marshall 3f91509
Deepak edits
dcherian 8be9289
bundle changesc
dcherian b289c20
add sts script, delete
e-marshall 4fb7c15
comment out bundles for now
e-marshall 07035a2
merged changes
e-marshall 287a7ba
draft of listdir
e-marshall 7abeee7
edits
e-marshall 7b47ade
Merge branch 'zarr-developers:v3' into stateful_store_tests
e-marshall 7be47c0
some changes
e-marshall 8902981
draft of stateful store tests
e-marshall a28f4b3
precommit
e-marshall 15adda3
Update src/zarr/strategies.py
e-marshall e873f68
Update tests/v3/test_store/test_stateful_store.py
e-marshall 2338158
Update tests/v3/test_store/test_stateful_store.py
e-marshall eaa6fd5
Apply suggestions from code review
e-marshall daa93d4
Apply suggestions from code review
e-marshall ba41d75
moved strategies and edits to stateful store tests
e-marshall fa457d0
Update tests/v3/test_store/test_stateful_store.py
e-marshall 05296c6
Update tests/v3/test_store/test_stateful_store.py
e-marshall d10cf5d
Update tests/v3/test_store/test_stateful_store.py
e-marshall 7f69dbb
Update tests/v3/test_store/test_stateful_store.py
e-marshall 923be62
Update tests/v3/test_store/test_stateful_store.py
e-marshall 7aceba6
Update tests/v3/test_store/test_stateful_store.py
e-marshall 0bd1932
Update tests/v3/test_store/test_stateful_store.py
e-marshall 619e043
fixed wrapper class and add rules for a few more methods
e-marshall 8e6f03c
incorporate Deepak's edits
e-marshall b21731f
switch to bundles + a few more updates
e-marshall 0065523
remove some notes
e-marshall 8504894
Update src/zarr/testing/strategies.py
e-marshall d298ed1
remove bundles
e-marshall 97ce42a
fix get signature, get invalid keys and some other changes
e-marshall 8d10f23
Merge branch 'v3' into stateful_store_tests
e-marshall 0efb594
fix get signature, get invalid keys and some other changes
e-marshall 391d1fd
add docstrings
e-marshall 087ddb6
finish merge
e-marshall 557ac85
Deepak edits
e-marshall d1711cb
fixing imports now that strategies in testing
e-marshall 7b3920b
Merge branch 'v3' into stateful_store_tests
dcherian f6c5b8d
revert docstrings
e-marshall 3cdf55f
Update run-hypothesis command
dcherian c2261de
Fix run-hypothesis command
dcherian 28811db
Apply suggestions from code review
e-marshall d63df09
changes from Joe's review
e-marshall 622d685
Apply suggestions from code review
e-marshall 8d2780a
add review comments
e-marshall 636a82f
Merge branch 'v3' into stateful_store_tests
dcherian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
# Stateful tests for arbitrary Zarr stores. | ||
|
||
import asyncio | ||
|
||
import hypothesis.strategies as st | ||
from hypothesis import assume, note | ||
from hypothesis.stateful import ( | ||
RuleBasedStateMachine, | ||
invariant, | ||
precondition, | ||
rule, | ||
) | ||
|
||
from zarr.buffer import Buffer, BufferPrototype, default_buffer_prototype | ||
from zarr.store import MemoryStore | ||
from zarr.testing.strategies import key_ranges, paths | ||
|
||
# zarr spec: https://zarr-specs.readthedocs.io/en/latest/v3/core/v3.0.html | ||
|
||
|
||
class SyncStoreWrapper: | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def __init__(self, store): | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Class to hold sync functions that map to async methods of MemoryStore | ||
MemoryStore methods are async, this class' methods are sync, so just need to call asyncio.run() in them | ||
then, methods in statemachine class are sync and call sync. | ||
Unfortunately, hypothesis' stateful testing infra does not support asyncio | ||
So we redefine sync versions of the Store API. | ||
https://github.com/HypothesisWorks/hypothesis/issues/3712#issuecomment-1668999041 | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
self.store = store | ||
self.mode = store.mode | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def set(self, key, data_buffer): | ||
return asyncio.run(self.store.set(key, data_buffer)) | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def list(self): | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
async def wrapper(gen): | ||
return [i async for i in gen] | ||
|
||
gen = self.store.list() | ||
yield from asyncio.run(wrapper(gen)) | ||
|
||
def get(self, key, prototype: BufferPrototype): | ||
obs = asyncio.run(self.store.get(key, prototype=prototype)) | ||
return obs | ||
|
||
def get_partial_values(self, key_ranges, prototype: BufferPrototype): | ||
obs_partial = asyncio.run( | ||
self.store.get_partial_values(prototype=prototype, key_ranges=key_ranges) | ||
) | ||
return obs_partial | ||
|
||
def delete(self, path): | ||
return asyncio.run(self.store.delete(path)) | ||
|
||
def empty(self): | ||
return asyncio.run(self.store.empty()) | ||
|
||
def clear(self): | ||
return asyncio.run(self.store.clear()) | ||
|
||
def exists(self, key): | ||
return asyncio.run(self.store.exists(key)) | ||
|
||
def list_dir(self, prefix): | ||
raise NotImplementedError | ||
|
||
def list_prefix(self, prefix: str): | ||
raise NotImplementedError | ||
|
||
def set_partial_values(self, key_start_values): | ||
raise NotImplementedError | ||
|
||
def supports_listing(self): | ||
raise NotImplementedError | ||
|
||
def supports_partial_writes(self): | ||
raise NotImplementedError | ||
|
||
def supports_writes(self): | ||
raise NotImplementedError | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
class ZarrStoreStateMachine(RuleBasedStateMachine): | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def __init__(self): | ||
super().__init__() | ||
self.model: dict[str, bytes] = {} | ||
self.store = SyncStoreWrapper(MemoryStore(mode="w")) | ||
self.prototype = default_buffer_prototype() | ||
|
||
@rule(key=paths, data=st.binary(min_size=0, max_size=100)) | ||
def set(self, key: str, data: bytes) -> None: | ||
note(f"(set) Setting {key!r} with {data}") | ||
assert not self.store.mode.readonly | ||
data_buf = Buffer.from_bytes(data) | ||
self.store.set(key, data_buf) | ||
self.model[key] = data_buf | ||
|
||
@precondition(lambda self: len(self.model.keys()) > 0) | ||
@rule(key=paths, data=st.data()) | ||
def get(self, key, data) -> None: | ||
key = data.draw( | ||
st.sampled_from(sorted(self.model.keys())) | ||
) # hypothesis wants to sample from sorted list | ||
model_value = self.model[key] | ||
note(f"(get) model value {model_value.to_bytes()}") | ||
store_value = self.store.get(key, self.prototype) | ||
note(f"(get) store value: {store_value.to_bytes()}") | ||
# to bytes here necessary because data_buf set to model in set() | ||
assert self.model[key].to_bytes() == (store_value.to_bytes()) | ||
|
||
@rule(key=paths, data=st.data()) | ||
def get_invalid_keys(self, key, data) -> None: | ||
model_keys = list(self.model.keys()) | ||
# model_keys = ['/'] | ||
# key = '/' | ||
note("(get_invalid)") | ||
|
||
# note(f"(get invalid) key: {key}") | ||
# note(f"(get invalid) val: {self.store.get(key, self.prototype)}") | ||
|
||
assume(key not in model_keys) | ||
assert self.store.get(key, self.prototype) is None | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@precondition(lambda self: len(self.model.keys()) > 0) | ||
@rule(data=st.data()) | ||
def get_partial_values(self, data) -> None: | ||
key_range = data.draw(key_ranges(keys=st.sampled_from(sorted(self.model.keys())))) | ||
note(f"(get partial) {key_range=}") | ||
obs_maybe = self.store.get_partial_values(key_range, self.prototype) | ||
observed = [] | ||
|
||
for obs in obs_maybe: | ||
assert obs is not None | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
observed.append(obs.to_bytes()) | ||
|
||
model_vals_ls = [] | ||
|
||
for key, byte_range in key_range: | ||
model_vals = self.model[key] | ||
start = byte_range[0] or 0 | ||
step = byte_range[1] | ||
stop = start + step if step is not None else None | ||
model_vals_ls.append(model_vals[start:stop]) | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
assert all( | ||
obs == exp.to_bytes() for obs, exp in zip(observed, model_vals_ls, strict=True) | ||
), ( | ||
observed, | ||
model_vals_ls, | ||
) | ||
|
||
@precondition(lambda self: len(self.model.keys()) > 0) | ||
@rule(data=st.data()) | ||
def delete(self, data) -> None: | ||
key = data.draw(st.sampled_from(sorted(self.model.keys()))) | ||
note(f"(delete) Deleting {key=}") | ||
|
||
self.store.delete(key) | ||
del self.model[key] | ||
|
||
@rule() | ||
def clear(self): | ||
assert not self.store.mode.readonly | ||
note("(clear)") | ||
self.store.clear() | ||
self.model.clear() | ||
|
||
assert len(self.model.keys()) == len(list(self.store.list())) == 0 | ||
|
||
@rule() | ||
def empty(self) -> None: | ||
note("(empty)") | ||
|
||
# make sure they either both are or both aren't empty (same state) | ||
assert self.store.empty() == (not self.model) | ||
|
||
@rule(key=paths) | ||
def exists(self, key) -> None: | ||
note("(exists)") | ||
|
||
assert self.store.exists(key) == (key in self.model) | ||
e-marshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@invariant() | ||
def check_paths_equal(self) -> None: | ||
note("Checking that paths are equal") | ||
paths = list(self.store.list()) | ||
|
||
assert list(self.model.keys()) == paths | ||
|
||
@invariant() | ||
def check_vals_equal(self) -> None: | ||
note("Checking values equal") | ||
for key, _val in self.model.items(): | ||
store_item = self.store.get(key, self.prototype).to_bytes() | ||
assert self.model[key].to_bytes() == store_item | ||
|
||
@invariant() | ||
def check_num_keys_equal(self) -> None: | ||
note("check num keys equal") | ||
|
||
assert len(self.model) == len(list(self.store.list())) | ||
|
||
@invariant() | ||
def check_keys(self) -> None: | ||
keys = list(self.store.list()) | ||
|
||
if len(keys) == 0: | ||
assert self.store.empty() is True | ||
|
||
elif len(keys) != 0: | ||
assert self.store.empty() is False | ||
|
||
for key in keys: | ||
assert self.store.exists(key) is True | ||
note("checking keys / exists / empty") | ||
|
||
|
||
StatefulStoreTest = ZarrStoreStateMachine.TestCase |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.