Skip to content

Commit fccd263

Browse files
committed
Add hypothesis testing
1 parent 1e85c59 commit fccd263

File tree

2 files changed

+139
-0
lines changed

2 files changed

+139
-0
lines changed

properties/test_parallelcompat.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import numpy as np
2+
import pytest
3+
4+
pytest.importorskip("hypothesis")
5+
# isort: split
6+
7+
from hypothesis import given
8+
9+
import xarray.testing.strategies as xrst
10+
from xarray.namedarray.parallelcompat import ChunkManagerEntrypoint
11+
12+
13+
class TestPreserveChunks:
14+
@given(xrst.shape_and_chunks())
15+
def test_preserve_all_chunks(
16+
self, shape_and_chunks: tuple[tuple[int, ...], tuple[int, ...]]
17+
) -> None:
18+
shape, previous_chunks = shape_and_chunks
19+
typesize = 8
20+
target = 1024 * 1024
21+
22+
actual = ChunkManagerEntrypoint.preserve_chunks(
23+
chunks=("preserve",) * len(shape),
24+
shape=shape,
25+
target=target,
26+
typesize=typesize,
27+
previous_chunks=previous_chunks,
28+
)
29+
for i, chunk in enumerate(actual):
30+
if chunk != shape[i]:
31+
assert chunk >= previous_chunks[i]
32+
assert chunk % previous_chunks[i] == 0
33+
assert chunk <= shape[i]
34+
35+
if actual != shape:
36+
assert np.prod(actual) * typesize >= 0.5 * target
37+
38+
@pytest.mark.parametrize("first_chunk", [-1, (), 1])
39+
@given(xrst.shape_and_chunks(min_dims=2))
40+
def test_preserve_some_chunks(
41+
self,
42+
first_chunk: int | tuple[int, ...],
43+
shape_and_chunks: tuple[tuple[int, ...], tuple[int, ...]],
44+
) -> None:
45+
shape, previous_chunks = shape_and_chunks
46+
typesize = 4
47+
target = 2 * 1024 * 1024
48+
49+
actual = ChunkManagerEntrypoint.preserve_chunks(
50+
chunks=(first_chunk, *["preserve" for _ in range(len(shape) - 1)]),
51+
shape=shape,
52+
target=target,
53+
typesize=typesize,
54+
previous_chunks=previous_chunks,
55+
)
56+
for i, chunk in enumerate(actual):
57+
if i == 0:
58+
if first_chunk == 1:
59+
assert chunk == 1
60+
elif first_chunk == -1:
61+
assert chunk == shape[i]
62+
elif first_chunk == ():
63+
assert chunk == previous_chunks[i]
64+
elif chunk != shape[i]:
65+
assert chunk >= previous_chunks[i]
66+
assert chunk % previous_chunks[i] == 0
67+
assert chunk <= shape[i]
68+
69+
# if we have more than one chunk, make sure the chunks are big enough
70+
if actual[1:] != shape[1:]:
71+
assert np.prod(actual) * typesize >= 0.5 * target

xarray/testing/strategies.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"names",
3232
"outer_array_indexers",
3333
"pandas_index_dtypes",
34+
"shape_and_chunks",
3435
"supported_dtypes",
3536
"unique_subset_of",
3637
"variables",
@@ -210,6 +211,73 @@ def dimension_sizes(
210211
)
211212

212213

214+
@st.composite
215+
def shape_and_chunks(
216+
draw: st.DrawFn,
217+
*,
218+
min_dims: int = 1,
219+
max_dims: int = 4,
220+
min_size: int = 1,
221+
max_size: int = 900,
222+
) -> tuple[tuple[int, ...], tuple[int, ...]]:
223+
"""
224+
Generate a shape tuple and corresponding chunks tuple.
225+
226+
Each element in the chunks tuple is smaller than or equal to the
227+
corresponding element in the shape tuple.
228+
229+
Requires the hypothesis package to be installed.
230+
231+
Parameters
232+
----------
233+
min_dims : int, optional
234+
Minimum number of dimensions. Default is 1.
235+
max_dims : int, optional
236+
Maximum number of dimensions. Default is 4.
237+
min_size : int, optional
238+
Minimum size for each dimension. Default is 1.
239+
max_size : int, optional
240+
Maximum size for each dimension. Default is 100.
241+
242+
Returns
243+
-------
244+
tuple[tuple[int, ...], tuple[int, ...]]
245+
A tuple containing (shape, chunks) where:
246+
- shape is a tuple of positive integers
247+
- chunks is a tuple where each element is an integer <= corresponding shape element
248+
249+
Examples
250+
--------
251+
>>> shape_and_chunks().example() # doctest: +SKIP
252+
((5, 3, 8), (2, 3, 4))
253+
>>> shape_and_chunks().example() # doctest: +SKIP
254+
((10, 7), (10, 3))
255+
>>> shape_and_chunks(min_dims=2, max_dims=3).example() # doctest: +SKIP
256+
((4, 6, 2), (2, 3, 1))
257+
258+
See Also
259+
--------
260+
:ref:`testing.hypothesis`_
261+
"""
262+
# Generate the shape tuple
263+
ndim = draw(st.integers(min_value=min_dims, max_value=max_dims))
264+
shape = draw(
265+
st.tuples(
266+
*[st.integers(min_value=min_size, max_value=max_size) for _ in range(ndim)]
267+
)
268+
)
269+
270+
# Generate chunks tuple with each element <= corresponding shape element
271+
chunks_elements = []
272+
for size in shape:
273+
# Each chunk is an integer between 1 and the size of that dimension
274+
chunk_element = draw(st.integers(min_value=1, max_value=size))
275+
chunks_elements.append(chunk_element)
276+
277+
chunks = tuple(chunks_elements)
278+
return shape, chunks
279+
280+
213281
_readable_strings = st.text(
214282
_readable_characters,
215283
max_size=5,

0 commit comments

Comments
 (0)