diff --git a/changes/216.misc.md b/changes/216.misc.md new file mode 100644 index 0000000000..e24fa7ade1 --- /dev/null +++ b/changes/216.misc.md @@ -0,0 +1 @@ +Convert six tests that loop over runtime-discovered collections (module exports, introspected config defaults, created hierarchy nodes, random byte ranges) to pytest's native `subtests` fixture, so each failing item is reported individually instead of the first failure masking the rest. diff --git a/tests/test_array.py b/tests/test_array.py index 09e7732960..fde1aab528 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -142,7 +142,7 @@ def test_array_creation_existing_node( @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_create_creates_parents( - store: LocalStore | MemoryStore, zarr_format: ZarrFormat + store: LocalStore | MemoryStore, zarr_format: ZarrFormat, subtests: pytest.Subtests ) -> None: # prepare a root node, with some data set await zarr.api.asynchronous.open_group( @@ -175,8 +175,10 @@ async def test_create_creates_parents( paths = ["a", "a/b", "a/b/c"] for path in paths: - g = await zarr.api.asynchronous.open_group(store=store, path=path) - assert isinstance(g, AsyncGroup) + # each parent is an independent claim; a missing intermediate must not mask the rest + with subtests.test(path=path): + g = await zarr.api.asynchronous.open_group(store=store, path=path) + assert isinstance(g, AsyncGroup) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) diff --git a/tests/test_coalesce.py b/tests/test_coalesce.py index cb8ff29ec7..78c152f6dd 100644 --- a/tests/test_coalesce.py +++ b/tests/test_coalesce.py @@ -518,8 +518,11 @@ async def test_fetch_raises_propagates() -> None: # --------------------------------------------------------------------------- -async def test_coverage_invariant_random_inputs() -> None: - """For any random RangeByteRequest input, every input index appears exactly once.""" +async def test_coverage_invariant_random_inputs(subtests: pytest.Subtests) -> None: + """For any random RangeByteRequest input, every input index appears exactly once. + + Each range's content check runs in its own subtest identified by its bounds, so a + single wrong range reports itself instead of hiding the other 49.""" import random rng = random.Random(42) @@ -538,7 +541,8 @@ async def test_coverage_invariant_random_inputs() -> None: flat = _contents(groups) for i, r in enumerate(ranges): assert isinstance(r, RangeByteRequest) - assert flat[i] == _INDEXED_BLOB[r.start : r.end] + with subtests.test(index=i, start=r.start, end=r.end): + assert flat[i] == _INDEXED_BLOB[r.start : r.end] # --------------------------------------------------------------------------- diff --git a/tests/test_config.py b/tests/test_config.py index a758378dc7..01429b9a45 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -112,10 +112,11 @@ def test_config_defaults_set() -> None: assert config.get("json_indent") == 2 -def test_array_config_init_defaults_match_global_config() -> None: +def test_array_config_init_defaults_match_global_config(subtests: pytest.Subtests) -> None: """Each `ArrayConfig.__init__` parameter that has a default must match the value of `array.` in the global config. Catches drift between - the two sources of truth.""" + the two sources of truth; each parameter is checked in its own subtest so + one drifted default does not mask the others.""" params = inspect.signature(ArrayConfig.__init__).parameters has_defaults = { name: p.default @@ -124,11 +125,8 @@ def test_array_config_init_defaults_match_global_config() -> None: } assert has_defaults, "expected at least one default to check" for name, default in has_defaults.items(): - assert default == config.get(f"array.{name}"), ( - f"ArrayConfig.__init__ default for {name!r} ({default!r}) does not " - f"match global config value for 'array.{name}' " - f"({config.get(f'array.{name}')!r})" - ) + with subtests.test(name=name): + assert default == config.get(f"array.{name}") @pytest.mark.parametrize( diff --git a/tests/test_group.py b/tests/test_group.py index 1acd5551ca..1001945409 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -86,7 +86,9 @@ def test_group_init(store: Store, zarr_format: ZarrFormat) -> None: assert group._async_group == agroup -async def test_create_creates_parents(store: Store, zarr_format: ZarrFormat) -> None: +async def test_create_creates_parents( + store: Store, zarr_format: ZarrFormat, subtests: pytest.Subtests +) -> None: # prepare a root node, with some data set await zarr.api.asynchronous.open_group( store=store, path="a", zarr_format=zarr_format, attributes={"key": "value"} @@ -128,14 +130,16 @@ async def test_create_creates_parents(store: Store, zarr_format: ZarrFormat) -> paths = ["a", "a/b", "a/b/c"] for path in paths: - g = await zarr.api.asynchronous.open_group(store=store, path=path) - assert isinstance(g, AsyncGroup) - - if path == "a": - # ensure we didn't overwrite the root attributes - assert g.attrs == {"key": "value"} - else: - assert g.attrs == {} + # each parent is an independent claim; a missing intermediate must not mask the rest + with subtests.test(path=path): + g = await zarr.api.asynchronous.open_group(store=store, path=path) + assert isinstance(g, AsyncGroup) + + if path == "a": + # ensure we didn't overwrite the root attributes + assert g.attrs == {"key": "value"} + else: + assert g.attrs == {} @pytest.mark.parametrize("store", ["memory"], indirect=True) @@ -1862,6 +1866,7 @@ async def test_group_create_hierarchy( overwrite: bool, group_path: str, impl: Literal["async", "sync"], + subtests: pytest.Subtests, ) -> None: """ Test that the Group.create_hierarchy method creates specified nodes and returns them in a dict. @@ -1895,15 +1900,18 @@ async def test_group_create_hierarchy( nodes_created = dict(g.create_hierarchy(node_spec, overwrite=overwrite)) all_members = dict(g.members(max_depth=None)) + # each node is an independent claim; one wrong node must not mask the others for k, v in node_spec.items(): - assert all_members[k].metadata == v == nodes_created[k].metadata + with subtests.test(node=k): + assert all_members[k].metadata == v == nodes_created[k].metadata # if overwrite is True, the extant nodes should be erased for k, v in extant_spec.items(): - if overwrite: - assert k in all_members - else: - assert all_members[k].metadata == v == extant_created[k].metadata + with subtests.test(extant_node=k): + if overwrite: + assert k in all_members + else: + assert all_members[k].metadata == v == extant_created[k].metadata # ensure that we left the root group as-is assert ( sync_group.get_node(store=store, path=group_path, zarr_format=zarr_format).attrs.asdict() diff --git a/tests/test_zarr.py b/tests/test_zarr.py index f49873132e..6476817ab8 100644 --- a/tests/test_zarr.py +++ b/tests/test_zarr.py @@ -3,14 +3,16 @@ import zarr -def test_exports() -> None: +def test_exports(subtests: pytest.Subtests) -> None: """ - Ensure that everything in __all__ can be imported. + Ensure that everything in __all__ can be imported. Each export is checked in its own + subtest so one broken export does not mask the others. """ from zarr import __all__ for export in __all__: - getattr(zarr, export) + with subtests.test(export=export): + getattr(zarr, export) def test_print_debug_info(capsys: pytest.CaptureFixture[str]) -> None: