Skip to content

Commit

Permalink
Add example hypothesis roundtrip test
Browse files Browse the repository at this point in the history
  • Loading branch information
dcherian committed Apr 5, 2024
1 parent 76c3450 commit 15788f5
Showing 1 changed file with 109 additions and 0 deletions.
109 changes: 109 additions & 0 deletions tests/test_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import pytest

pytest.importorskip("hypothesis")

import zarr
import numpy as np

from hypothesis import settings
import hypothesis.strategies as st
import hypothesis.extra.numpy as npst
from hypothesis import given, settings
from zarr.core import Array

from zarr._storage.v3 import KVStoreV3
from zarr.storage import init_array, normalize_store_arg

#### TODO: Provide this in zarr.strategies
# Copied from Xarray
# only use characters within the "Latin Extended-A" subset of unicode
_readable_characters = st.characters(categories=["L", "N"], max_codepoint=0x017F)
_readable_strings = st.text(_readable_characters, max_size=5)
_attr_keys = st.text(_readable_characters, min_size=1)
_attr_values = st.recursive(
st.none() | st.booleans() | _readable_strings,
lambda children: st.lists(children) | st.dictionaries(_attr_keys, children),
max_leaves=3,
)

# No '/' in array names?
# No '.' in paths?
zarr_key_chars = st.sampled_from("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz")

# The following should be public strategies
attrs = st.none() | st.dictionaries(_attr_keys, _attr_values)
paths = st.none() | st.text(zarr_key_chars, min_size=1) | st.just("/")
array_names = st.text(zarr_key_chars | st.just("."), min_size=1).filter(
lambda t: not t.startswith((".", ".."))
)


@st.composite
def array_strategy(draw):
"""A hypothesis strategy to generate small sized random arrays.
Returns: a tuple of the array and a suitable random chunking for it.
"""
array = draw(npst.arrays(dtype=npst.scalar_dtypes(), shape=npst.array_shapes(max_dims=4)))
# We want this strategy to shrink towards arrays with smaller number of chunks
# 1. st.integers() shrinks towards smaller values. So we use that to generate number of chunks
numchunks = draw(st.tuples(*[st.integers(min_value=1, max_value=size) for size in array.shape]))
# 2. and now generate the chunks tuple
chunks = tuple(size // nchunks for size, nchunks in zip(array.shape, numchunks))
return (array, chunks)


#####


# @pytest.mark.slow
@settings(max_examples=300)
@given(path=paths, attrs=attrs, name=array_names, array_and_chunks=array_strategy())
def test_roundtrip(path, array_and_chunks, name, attrs):
store = KVStoreV3(dict())

nparray, chunks = array_and_chunks

# TODO: clean this up
if path is None and name is None:
array_path = None
array_name = None
elif path is None and name is not None:
array_path = f"{name}"
array_name = f"/{name}"
elif path is not None and name is None:
array_path = path
array_name = None
elif path == "/":
assert name is not None
array_path = name
array_name = "/" + name
else:
assert name is not None
array_path = f"{path}/{name}"
array_name = "/" + array_path

expected_attrs = {} if attrs is None else attrs

init_array(store, shape=nparray.shape, chunks=chunks, path=array_path, dtype=nparray.dtype.str)
a = Array(store, path=array_path, zarr_version=3)
if attrs is not None:
a.attrs.put(attrs)

assert isinstance(a, Array)
assert nparray.shape == a.shape
assert chunks == a.chunks
assert array_path == a.path
assert array_name == a.name
# assert a.basename is None # TODO
assert a.store == normalize_store_arg(store)
assert a.attrs.asdict() == expected_attrs

a[:] = nparray

store.close()

group = zarr.open_group(store)
actual = group[array_path]
assert actual.attrs.asdict() == expected_attrs
np.testing.assert_equal(nparray, np.array(actual))

0 comments on commit 15788f5

Please sign in to comment.