Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
be22268
docs: design spec for indexing test cleanup
d-v-b May 22, 2026
6c50c9e
docs: implementation plan for indexing test cleanup
d-v-b May 22, 2026
72bc014
test: make canonical Expect/ExpectFail frozen
d-v-b May 22, 2026
1d03c1f
test: migrate test_chunk_grids to canonical Expect/ExpectFail
d-v-b May 22, 2026
6918872
test: migrate test_cast_value to canonical Expect/ExpectFail
d-v-b May 22, 2026
97dbb4c
test: migrate test_scale_offset to canonical Expect/ExpectFail
d-v-b May 22, 2026
e397598
test: delete duplicate Expect dataclasses in test_codecs/conftest.py
d-v-b May 22, 2026
af8c302
test: rewrite orthogonal 1d bool indexing as parametrized cases
d-v-b May 22, 2026
db55885
docs: add Task 0.6 (optional ExpectFail.msg + raises helper)
d-v-b May 22, 2026
967ff21
test: make ExpectFail.msg optional with a raises() helper
d-v-b May 22, 2026
f51b0ec
test: clearer single-true mask idiom in orthogonal 1d exemplar
d-v-b May 22, 2026
524e88e
test: rewrite orthogonal 1d int indexing as parametrized cases
d-v-b May 22, 2026
33cbf0a
test: rewrite orthogonal 2d indexing as parametrized cases
d-v-b May 22, 2026
9bf028b
test: rewrite orthogonal 3d indexing as parametrized cases
d-v-b May 22, 2026
482f7b4
test: rewrite set-orthogonal indexing family as parametrized cases
d-v-b May 22, 2026
0cec6a8
test: rewrite basic 1d/2d selection families as parametrized cases
d-v-b May 22, 2026
3539816
test: restore basic-indexing integer-list rejection coverage
d-v-b May 22, 2026
b96b289
test: rewrite coordinate selection family as parametrized cases
d-v-b May 22, 2026
dad6aca
test: rewrite block selection family as parametrized cases
d-v-b May 22, 2026
88e574b
test: rewrite mask selection family as parametrized cases
d-v-b May 22, 2026
3c7026e
test: shrink arrays in selection_out and numpy-equivalence tests
d-v-b May 22, 2026
ed346c5
test: add behavior docstrings to remaining indexing tests
d-v-b May 22, 2026
1b7611d
test: drop redundant pytest.mark.asyncio decorators
d-v-b May 22, 2026
5e2ff9a
docs: add changelog fragment for indexing test cleanup
d-v-b May 22, 2026
ac0989e
test: explain msg=None on the basic-1d string bad case
d-v-b May 22, 2026
e271a5a
docs: remove LLM plans
d-v-b May 22, 2026
da7ce66
docs: rename changelog
d-v-b May 22, 2026
b4892af
Merge branch 'main' into indexing-test-cleanup
d-v-b May 22, 2026
ba19fe2
Merge branch 'main' into indexing-test-cleanup
d-v-b May 27, 2026
fa983f5
Merge branch 'main' into indexing-test-cleanup
d-v-b May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/4001.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Consolidated the array indexing test suite (`tests/test_indexing.py`): the loop-and-`np.random` based selection tests were rewritten as deterministic, parametrized `Expect`/`ExpectFail` cases on small arrays, error paths were split into their own named tests, and the two divergent `Expect` test-case dataclass pairs were unified onto the canonical one in `tests/conftest.py` (whose `ExpectFail` now has an optional regex `msg` and a `raises()` helper). Test-only change with no effect on the public API.
23 changes: 19 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import math
import os
import pathlib
import re
import sys
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
Expand Down Expand Up @@ -50,6 +51,7 @@

if TYPE_CHECKING:
from collections.abc import Generator
from contextlib import AbstractContextManager
from typing import Any, Literal

from _pytest.compat import LEGACY_PATH
Expand All @@ -64,7 +66,7 @@
from zarr.core.dtype.wrapper import ZDType


@dataclass
@dataclass(frozen=True)
class Expect[TIn, TOut]:
"""A test case with explicit input, expected output, and a human-readable id."""

Expand All @@ -73,14 +75,27 @@ class Expect[TIn, TOut]:
id: str


@dataclass
@dataclass(frozen=True)
class ExpectFail[TIn]:
"""A test case that should raise an exception."""
"""A test case that should raise an exception.

`msg` is a regex matched against the exception text (pytest's native
`match=` semantics). Leave it `None` to assert only the exception type. Set
`escape=True` when `msg` is a literal that contains regex metacharacters
such as `(`, `[`, or `.`; `escape` has no effect when `msg` is `None`.
"""

input: TIn
exception: type[Exception]
id: str
msg: str
msg: str | None = None
escape: bool = False

def raises(self) -> AbstractContextManager[pytest.ExceptionInfo[Exception]]:
if self.msg is None:
return pytest.raises(self.exception)
pattern = re.escape(self.msg) if self.escape else self.msg
return pytest.raises(self.exception, match=pattern)


async def parse_store(
Expand Down
110 changes: 66 additions & 44 deletions tests/test_chunk_grids.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import re
from typing import Any

import numpy as np
import pytest

from tests.test_codecs.conftest import Expect, ExpectErr
from tests.conftest import Expect, ExpectFail
from zarr.core.chunk_grids import (
ChunkLayout,
_guess_regular_chunks,
Expand Down Expand Up @@ -131,95 +130,118 @@ def test_chunk_layout_nested() -> None:
@pytest.mark.parametrize(
"case",
[
ExpectErr(input=(0, 100), msg="Chunk size must be positive", exception_cls=ValueError),
ExpectErr(input=(-2, 100), msg="Chunk size must be positive", exception_cls=ValueError),
ExpectErr(input=([], 100), msg="must not be empty", exception_cls=ValueError),
ExpectErr(input=([10, -1, 10], 100), msg="must be positive", exception_cls=ValueError),
ExpectErr(input=([10, 0, 10], 20), msg="must be positive", exception_cls=ValueError),
ExpectErr(input=([10, 20], 100), msg="do not sum to span", exception_cls=ValueError),
ExpectFail(
input=(0, 100),
exception=ValueError,
id="zero-uniform",
msg="Chunk size must be positive",
),
ExpectFail(
input=(-2, 100),
exception=ValueError,
id="negative-uniform",
msg="Chunk size must be positive",
),
ExpectFail(input=([], 100), exception=ValueError, id="empty-list", msg="must not be empty"),
ExpectFail(
input=([10, -1, 10], 100),
exception=ValueError,
id="negative-element",
msg="must be positive",
),
ExpectFail(
input=([10, 0, 10], 20), exception=ValueError, id="zero-element", msg="must be positive"
),
ExpectFail(
input=([10, 20], 100), exception=ValueError, id="wrong-sum", msg="do not sum to span"
),
# Nested/RLE form for a single dim is rejected with offending indices.
ExpectErr(
ExpectFail(
input=([[3, 3], 1], 7),
exception=TypeError,
id="rle-single-dim",
msg="non-integer element(s) ([3, 3],) at indices (0,)",
exception_cls=TypeError,
escape=True,
),
# Multiple non-int elements: all offending indices reported.
ExpectErr(
ExpectFail(
input=([1, [2, 2], 1, [3]], 9),
exception=TypeError,
id="multiple-non-ints",
msg="non-integer element(s) ([2, 2], [3]) at indices (1, 3)",
exception_cls=TypeError,
escape=True,
),
# Strings are non-integers and should be reported the same way.
ExpectErr(
ExpectFail(
input=([2, "3", 5], 10),
exception=TypeError,
id="string-element",
msg="non-integer element(s) ('3',) at indices (1,)",
exception_cls=TypeError,
escape=True,
),
],
ids=[
"zero-uniform",
"negative-uniform",
"empty-list",
"negative-element",
"zero-element",
"wrong-sum",
"rle-single-dim",
"multiple-non-ints",
"string-element",
],
ids=lambda c: c.id,
)
def test_normalize_chunks_1d_errors(case: ExpectErr[tuple[Any, int]]) -> None:
def test_normalize_chunks_1d_errors(case: ExpectFail[tuple[Any, int]]) -> None:
"""Invalid 1D chunk specifications are rejected with informative error messages."""
chunks, span = case.input
with pytest.raises(case.exception_cls, match=re.escape(case.msg)):
with case.raises():
normalize_chunks_1d(chunks, span=span)


@pytest.mark.parametrize(
"case",
[
ExpectErr(
ExpectFail(
input=(None, (100,)),
exception=ValueError,
id="none",
msg="None is not a valid chunk input",
exception_cls=ValueError,
),
# `True` is rejected explicitly because bool is a subclass of int — without
# this guard, `chunks=True` would silently produce size-1 chunks.
ExpectErr(
ExpectFail(
input=(True, (100,)),
exception=ValueError,
id="true",
msg="True is not a valid chunk input",
exception_cls=ValueError,
),
ExpectErr(input=("foo", (100,)), msg="dimensions", exception_cls=ValueError),
ExpectErr(input=((100, 10), (100,)), msg="dimensions", exception_cls=ValueError),
ExpectErr(input=((10,), (100, 100)), msg="dimensions", exception_cls=ValueError),
ExpectFail(input=("foo", (100,)), exception=ValueError, id="string", msg="dimensions"),
ExpectFail(
input=((100, 10), (100,)), exception=ValueError, id="too-many-dims", msg="dimensions"
),
ExpectFail(
input=((10,), (100, 100)), exception=ValueError, id="too-few-dims", msg="dimensions"
),
# End-to-end: per-dim RLE surfaces through normalize_chunks_nd.
ExpectErr(
ExpectFail(
input=([[6, 4], [[3, 3], 1]], (10, 10)),
exception=TypeError,
id="rle-inner-dim",
msg="non-integer element(s) ([3, 3],) at indices (0,)",
exception_cls=TypeError,
escape=True,
),
],
ids=["none", "true", "string", "too-many-dims", "too-few-dims", "rle-inner-dim"],
ids=lambda c: c.id,
)
def test_normalize_chunks_nd_errors(case: ExpectErr[tuple[Any, tuple[int, ...]]]) -> None:
def test_normalize_chunks_nd_errors(case: ExpectFail[tuple[Any, tuple[int, ...]]]) -> None:
"""Invalid N-D chunk specifications are rejected with informative error messages."""
chunks, shape = case.input
with pytest.raises(case.exception_cls, match=re.escape(case.msg)):
with case.raises():
normalize_chunks_nd(chunks, shape)


@pytest.mark.parametrize(
"case",
[
# uniform-chunks branch: one int → broadcast across span via np.full.
Expect(input=(1000, 100_000), expected=[1000] * 100),
Expect(input=(1000, 100_000), output=[1000] * 100, id="uniform"),
# explicit-per-chunk branch.
Expect(input=([10, 20, 30, 40], 100), expected=[10, 20, 30, 40]),
Expect(input=([10, 20, 30, 40], 100), output=[10, 20, 30, 40], id="explicit-list"),
# -1 sentinel branch: one chunk covering the full span.
Expect(input=(-1, 100), expected=[100]),
Expect(input=(-1, 100), output=[100], id="full-span-sentinel"),
],
ids=["uniform", "explicit-list", "full-span-sentinel"],
ids=lambda c: c.id,
)
def test_normalize_chunks_1d_returns_int64_array(
case: Expect[tuple[Any, int], list[int]],
Expand All @@ -230,4 +252,4 @@ def test_normalize_chunks_1d_returns_int64_array(
assert isinstance(result, np.ndarray)
assert result.dtype == np.int64
assert result.ndim == 1
assert result.tolist() == case.expected
assert result.tolist() == case.output
20 changes: 0 additions & 20 deletions tests/test_codecs/conftest.py

This file was deleted.

Loading
Loading