diff --git a/tests/test_properties.py b/tests/test_properties.py new file mode 100644 index 0000000000..b3152ea0e3 --- /dev/null +++ b/tests/test_properties.py @@ -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))