diff --git a/changes/215.misc.md b/changes/215.misc.md new file mode 100644 index 0000000000..f8df7cc632 --- /dev/null +++ b/changes/215.misc.md @@ -0,0 +1 @@ +Enable pytest's `strict = true` config option (strict config, markers, xfail, and parametrization ids), replacing the `--strict-config`/`--strict-markers` addopts flags that pytest silently ignored before 9.1, and fix the seven duplicate parametrization ids it surfaced — including restoring float-JSON roundtrip cases that were meant to cover numpy scalars but ran plain-float cases twice instead. Also arm pytest's faulthandler watchdog (`faulthandler_timeout = 600` with `faulthandler_exit_on_timeout`) so a deadlocked test dumps every thread's traceback and fails the run instead of hanging indefinitely. diff --git a/pyproject.toml b/pyproject.toml index 255d546276..081ddefdfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -426,11 +426,21 @@ module = [ ignore_errors = true [tool.pytest.ini_options] -minversion = "7" +minversion = "9" testpaths = ["src", "tests", "docs/user-guide"] log_cli_level = "INFO" log_level = "INFO" -xfail_strict = true +# Enables strict_config, strict_markers, strict_xfail, strict_parametrization_ids, and +# any strictness options added in future pytest releases. Note that the equivalent +# `--strict-config`/`--strict-markers` flags were silently ignored when passed via +# addopts before pytest 9.1 (pytest#14442), so this option is the reliable spelling. +strict = true +# Turn deadlocks into loud failures: if a single test exceeds this many seconds, dump +# every thread's traceback and kill the run (exit_on_timeout is new in pytest 9). Sized +# far above the slowest legitimate test (~20s locally; slower under coverage/Windows/ +# nightly stateful-hypothesis runs) so only a genuine hang can trip it. +faulthandler_timeout = 600 +faulthandler_exit_on_timeout = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" doctest_optionflags = [ @@ -442,7 +452,7 @@ addopts = [ "--benchmark-columns", "min,mean,stddev,outliers,rounds,iterations", "--benchmark-disable", # benchmark routines run as tests without benchmarking instrumentation "--durations", "10", - "-ra", "--strict-config", "--strict-markers", + "-ra", "--doctest-modules", "--ignore=tests/test_regression/scripts", "--ignore=src/zarr/_cli", diff --git a/tests/test_array.py b/tests/test_array.py index 89d7547e78..4faf99eb9e 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -1693,7 +1693,8 @@ def test_default_endianness( assert endianness_from_numpy_str(byte_order) == endianness # type: ignore[arg-type] -@pytest.mark.parametrize("value", [1, 1.4, "a", b"a", np.array(1)]) +# The explicit id for b"a" avoids colliding with the auto-generated id for "a". +@pytest.mark.parametrize("value", [1, 1.4, "a", pytest.param(b"a", id="a-bytes"), np.array(1)]) @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") def test_scalar_array(value: Any, zarr_format: ZarrFormat) -> None: diff --git a/tests/test_codecs/test_vlen.py b/tests/test_codecs/test_vlen.py index c2c2c9b201..b90ad88ddc 100644 --- a/tests/test_codecs/test_vlen.py +++ b/tests/test_codecs/test_vlen.py @@ -13,10 +13,12 @@ from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.storage import StorePath -numpy_str_dtypes: list[type | str | None] = [ +# The explicit id for the "str" literal avoids colliding with the auto-generated id for +# the `str` builtin. +numpy_str_dtypes: list[Any] = [ None, str, - "str", + pytest.param("str", id="str-literal"), np.dtypes.StrDType, "S", "U", diff --git a/tests/test_dtype/conftest.py b/tests/test_dtype/conftest.py index 4c585bfdf6..100b9df226 100644 --- a/tests/test_dtype/conftest.py +++ b/tests/test_dtype/conftest.py @@ -1,5 +1,6 @@ # Generate a collection of zdtype instances for use in testing. import warnings +from collections import Counter from typing import Any import numpy as np @@ -64,4 +65,19 @@ class TestB(TestExample): for fixture_name in metafunc.fixturenames: if hasattr(metafunc.cls, fixture_name): params = getattr(metafunc.cls, fixture_name) - metafunc.parametrize(fixture_name, params, scope="class", ids=str) + metafunc.parametrize( + fixture_name, params, scope="class", ids=_unique_ids([str(p) for p in params]) + ) + + +def _unique_ids(ids: list[str]) -> list[str]: + """Suffix repeated ids with their positional index so every id is unique. + + Distinct parameters can stringify identically: for example `np.dtype("i")` and + `np.dtype(" 1 else id_ for idx, id_ in enumerate(ids)] diff --git a/tests/test_dtype/test_npy/test_common.py b/tests/test_dtype/test_npy/test_common.py index d8912a70ec..b7e4e875ad 100644 --- a/tests/test_dtype/test_npy/test_common.py +++ b/tests/test_dtype/test_npy/test_common.py @@ -36,14 +36,17 @@ from zarr.core.common import JSON, ZarrFormat -json_float_v2_roundtrip_cases: tuple[tuple[JSONFloatV2, float | np.floating[Any]], ...] = ( - ("Infinity", float("inf")), - ("Infinity", np.inf), - ("-Infinity", float("-inf")), - ("-Infinity", -np.inf), - ("NaN", float("nan")), - ("NaN", np.nan), - (1.0, 1.0), +# Each special value is tested as both a Python float and a numpy scalar. The explicit +# ids are load-bearing: np.float64("inf") stringifies identically to float("inf"), so +# without them these parameter sets would produce duplicate test ids. +json_float_v2_roundtrip_cases: tuple[Any, ...] = ( + pytest.param("Infinity", float("inf"), id="Infinity-float"), + pytest.param("Infinity", np.float64("inf"), id="Infinity-float64"), + pytest.param("-Infinity", float("-inf"), id="-Infinity-float"), + pytest.param("-Infinity", np.float64("-inf"), id="-Infinity-float64"), + pytest.param("NaN", float("nan"), id="NaN-float"), + pytest.param("NaN", np.float64("nan"), id="NaN-float64"), + pytest.param(1.0, 1.0, id="1.0-1.0"), ) json_float_v3_cases = json_float_v2_roundtrip_cases diff --git a/tests/test_group.py b/tests/test_group.py index bc80e19e86..1acd5551ca 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1713,6 +1713,9 @@ def test_create_nodes_concurrency_limit(store: MemoryStore) -> None: (zarr.core.group.create_rooted_hierarchy, zarr.core.sync_group.create_rooted_hierarchy), (zarr.core.group.get_node, zarr.core.sync_group.get_node), ], + # The default ids (from __name__) collide: the method pair and the module-level pair + # for create_hierarchy would both be id'd "create_hierarchy-create_hierarchy". + ids=lambda func: f"{func.__module__.rsplit('.', maxsplit=1)[-1]}.{func.__qualname__}", ) def test_consistent_signatures( a_func: Callable[[object], object], b_func: Callable[[object], object] diff --git a/tests/test_metadata/test_v2.py b/tests/test_metadata/test_v2.py index d1a1ca00b4..0f280f0401 100644 --- a/tests/test_metadata/test_v2.py +++ b/tests/test_metadata/test_v2.py @@ -29,7 +29,8 @@ def test_parse_zarr_format_valid() -> None: assert parse_zarr_format(2) == 2 -@pytest.mark.parametrize("data", [None, 1, 3, 4, 5, "3"]) +# The explicit id for "3" avoids colliding with the auto-generated id for the int 3. +@pytest.mark.parametrize("data", [None, 1, 3, 4, 5, pytest.param("3", id="3-str")]) def test_parse_zarr_format_invalid(data: Any) -> None: with pytest.raises(ValueError, match=f"Invalid value. Expected 2. Got {data}"): parse_zarr_format(data)