Repository: HypothesisWorks/hypothesis

Issue:
Run tests for `hypothesis.extra.array_api` against *any* implementation
#3065 comes with a lovely test suite for our new `hypothesis.extra.array_api` module, with easy parameterization across implementations - but currently that's just for `numpy.array_api` and our own internal mock instance (which is built on standard Numpy).

It would be lovely to have some way to run these tests against other implementations of the Array API too!

Obvious ideas include an environment variable of some kind, and using the entry-points described in https://github.com/data-apis/array-api/issues/244 to run against everything that's installed.  (but we'd still want the env var, for libraries which haven't adopted the entrypoint yet)  This isn't urgent and so we left it out of #3065 for size, but would be very nice to have.


Discussion:
Just as we'd want to try multiple Array API implementations for our own test suite, so would developers of array-consuming libraries testing with Hypothesis—would it be reasonable to have a "generalised" Array API strategies that fuzz all available array/tensor libraries in the users env?

(Getting way ahead of myself of-course, just wanted to note this for the future when there's more adoption/entry-points in the spec.)
> Would it be reasonable to have a "generalised" Array API strategies that fuzz all available array/tensor libraries in the users env?

IMO whatever we'd want to do for that should just be an option on https://github.com/data-apis/array-api-tests - happy to have close integration/colaboration, but it's not really about testing *Hypothesis* at that point.


> would it be reasonable to have a "generalised" Array API strategies that fuzz all available array/tensor libraries in the users env?

Do you mean fuzz *against* all available libraries, or fuzz *using* all available libraries? If you mean the latter, e.g.:

```python
@given(all_arrays(dtype="float"))
def test_something_in_dask(x):
    # x : np.ndarray | torch.Tensor | mygrad.Tensor | <any other available array/tensor in my env>
``` 

I would vote "no" on that, for a few reasons. 

One big reason is: this test would become less and less incisive (and I believe the strategy would shrink less effectively) as I add more libraries to my environment; this is assuming that  `all_arrays` is somethings akin to `st.one_of(np_arrays(..), torch_tensors(...), mygrad_tensors(...))` and assuming that I keep `max_examples` the fixed. It would be a shame for a library's tests to get "weaker" as its maintainers aim to be increasingly thorough by testing against more array implementations. 

That being said, it might be worth documenting a recipe that enables maintainer to construct this sort of uber strategy themselves (and we can include appropriate warnings – "hey, this is a convenient way to do some shallow fuzzing using a bunch of implementations")

If that is not what you meant then.... well just ignore all of that  😅 

Yep I meant fuzz using all available libraries. What you say makes sense, thanks!

+1 for a recipe in the docs (again for the future). When I'm testing an array-consuming library I would appreciate a "shallow" `all_arrays()`-ish strategy, if just to smoke the methods I'm testing. Such a recipe could first include an `array_modules()` strategy that calls `st.sampled_from([numpy.array_api, ...])`—if entry points goes through, it might even be something appropriate for `extra.array_api`.
I think I'd prefer to make the "combination strategies" explicitly constructed; but I'd be fine with a pretty slim wrapper around `make_strategy_namespace()`, usable like:

```python
xps = array_combination_namespace(*array_modules)

xps.arrays(**kw)  # returns one_of([make_strategy_namespace(mod).arrays(**kw) for mod in array_modules])
```
[Entry points went through](https://data-apis.org/array-api/latest/purpose_and_scope.html#discoverability-of-conforming-implementations)! In https://github.com/HypothesisWorks/hypothesis/pull/3136#issuecomment-961547613 @Zac-HD suggested two options for next steps:
> 1. [Run tests for `hypothesis.extra.array_api` against *any* implementation #3085](https://github.com/HypothesisWorks/hypothesis/issues/3085), ensuring that we run our own selftests against all installed implementations.  I'm happy to rely on entry points for this; I'd prefer to ask array implementors to support it rather than checking a custom env var.
> 
>  2. Also [Run tests for `hypothesis.extra.array_api` against *any* implementation #3085](https://github.com/HypothesisWorks/hypothesis/issues/3085), in the comments, we should think about ways make testing against multiple array-api modules easy for our users.  For example we could add an `installed_array_modules()` function which loads them via the entry point and add `*args` to `make_strategies_namespace(xp)` pretty easily - the challenge is to work out the maximum feasible level of mixing (e.g. `xp1.arrays(xp2.float32, ...)` is probably bad?).

In any case, having `extra.array_api` able to hook into entry points somehow will let us practically do point 1 and give us options for point 2. Zac had tried a module-level `__getattr__` idea in https://github.com/HypothesisWorks/hypothesis/pull/3065#issuecomment-914086577 so they might like to do this. Otherwise, feel free to task me with giving this a go, and/or if it's more complicated than we can discuss this in a separate issue. Also, NumPy upstream can be used to actually see it works now.
@Zac-HD would you like me to have a stab at adding entry points support for `extra.array_api` by way of a module-level `__getattr__()` e.g. so we could have something like `extra.array_api.pytorch`. Wasn't sure if you already had something cooked, and I can still have a go regardless if you'd like.

And following on from that, I could try using it for `tests/array_api/`.
Let's take a more incremental approach than we've done before 😂 

1. New function `installed_array_modules() -> Dict[str, module]` which loads modules from entry points.  This is "just" a convenience wrapper over `importlib.metadata`, but worth it IMO since the API changed in 3.10.
2. Use this to parametrize our tests over all installed array modules.

After that we can think about whether it makes sense to add multi-module support to `make_strategies_namespace`.

IMO we *should not* do the `__getattr__` plan though; per https://github.com/HypothesisWorks/hypothesis/pull/3065#issuecomment-914086577 I just couldn't get it to fail gracefully (i.e. with a useful error message when used via `import`), and that's a hard requirement for me.
We got the internal entry point hook down, so now we need to

- [ ] Parametrize modules for `tests/array_api/`

I'll explore how to do this nicely in the next few days but imagine the PR will be for next year.

Wouldn't be helpful until CuPy gets entrypoints anyway (easily monkey patched for local testing tho), as beyond NumPy there's no other implemented-enough array module to use yet. Also @Zac-HD would you know yet if CuPy be possible on CI? It only _feasibly_ runs on Nvidia GPUs... I've been using a remote VM for dev haha.
I don't know, but it's good to have even if we can't run it in CI - I can run locally, and also ask if Nvidia will sponsor us some CI time to maintain good CuPy support. 

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py
--- a/hypothesis-python/src/hypothesis/extra/array_api.py
+++ b/hypothesis-python/src/hypothesis/extra/array_api.py
@@ -863,7 +863,11 @@ def floating_dtypes(
     unsigned_integer_dtypes.__doc__ = _unsigned_integer_dtypes.__doc__
     floating_dtypes.__doc__ = _floating_dtypes.__doc__
 
-    return SimpleNamespace(
+    class PrettySimpleNamespace(SimpleNamespace):
+        def __repr__(self):
+            return f"make_strategies_namespace({xp.__name__})"
+
+    return PrettySimpleNamespace(
         from_dtype=from_dtype,
         arrays=arrays,
         array_shapes=array_shapes,


Test patch:
diff --git a/hypothesis-python/tests/array_api/README.md b/hypothesis-python/tests/array_api/README.md
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/array_api/README.md
@@ -0,0 +1,32 @@
+This folder contains tests for `hypothesis.extra.array_api`.
+
+## Running against different array modules
+
+By default it will run against `numpy.array_api`. If that's not available
+(likely because an older NumPy version is installed), these tests will fallback
+to using the mock defined at the bottom of `src/hypothesis/extra/array_api.py`.
+
+You can test other array modules which adopt the Array API via the
+`HYPOTHESIS_TEST_ARRAY_API` environment variable. There are two recognized
+options:
+
+* `"default"`: only uses `numpy.array_api`, or if not available, fallbacks to the mock.
+* `"all"`: uses all array modules found via entry points, _and_ the mock.
+
+If neither of these, the test suite will then try resolve the variable like so:
+
+1. If the variable matches a name of an available entry point, load said entry point.
+2. If the variables matches a valid import path, import said path.
+
+For example, to specify NumPy's Array API implementation, you could use its
+entry point (**1.**),
+
+    HYPOTHESIS_TEST_ARRAY_API=numpy pytest tests/array_api
+
+or use the import path (**2.**),
+
+    HYPOTHESIS_TEST_ARRAY_API=numpy.array_api pytest tests/array_api
+
+The former method is more ergonomic, but as entry points are optional for
+adopting the Array API, you will need to use the latter method for libraries
+that opt-out.
diff --git a/hypothesis-python/tests/array_api/common.py b/hypothesis-python/tests/array_api/common.py
--- a/hypothesis-python/tests/array_api/common.py
+++ b/hypothesis-python/tests/array_api/common.py
@@ -10,19 +10,12 @@
 
 from importlib.metadata import EntryPoint, entry_points  # type: ignore
 from typing import Dict
-from warnings import catch_warnings
 
-import pytest
-
-from hypothesis.errors import HypothesisWarning
-from hypothesis.extra.array_api import make_strategies_namespace, mock_xp
 from hypothesis.internal.floats import next_up
 
 __all__ = [
-    "xp",
-    "xps",
-    "COMPLIANT_XP",
-    "WIDTHS_FTZ",
+    "installed_array_modules",
+    "flushes_to_zero",
 ]
 
 
@@ -44,25 +37,14 @@ def installed_array_modules() -> Dict[str, EntryPoint]:
     return {ep.name: ep for ep in eps}
 
 
-# We try importing the Array API namespace from NumPy first, which modern
-# versions should include. If not available we default to our own mocked module,
-# which should allow our test suite to still work. A constant is set accordingly
-# to inform our test suite of whether the array module here is a mock or not.
-modules = installed_array_modules()
-try:
-    with catch_warnings():  # NumPy currently warns on import
-        xp = modules["numpy"].load()
-except KeyError:
-    xp = mock_xp
-    with pytest.warns(HypothesisWarning):
-        xps = make_strategies_namespace(xp)
-    COMPLIANT_XP = False
-else:
-    xps = make_strategies_namespace(xp)
-    COMPLIANT_XP = True
+def flushes_to_zero(xp, width: int) -> bool:
+    """Infer whether build of array module has its float dtype of the specified
+    width flush subnormals to zero
 
-# Infer whether build of array module has its float flush subnormals to zero
-WIDTHS_FTZ = {
-    32: bool(xp.asarray(next_up(0.0, width=32), dtype=xp.float32) == 0),
-    64: bool(xp.asarray(next_up(0.0, width=64), dtype=xp.float64) == 0),
-}
+    We do this per-width because compilers might FTZ for one dtype but allow
+    subnormals in the other.
+    """
+    if width not in [32, 64]:
+        raise ValueError(f"{width=}, but should be either 32 or 64")
+    dtype = getattr(xp, f"float{width}")
+    return bool(xp.asarray(next_up(0.0, width=width), dtype=dtype) == 0)
diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/array_api/conftest.py
@@ -0,0 +1,72 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Copyright the Hypothesis Authors.
+# Individual contributors are listed in AUTHORS.rst and the git log.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+
+import warnings
+from importlib import import_module
+from os import getenv
+
+import pytest
+
+from hypothesis.errors import HypothesisWarning
+from hypothesis.extra.array_api import make_strategies_namespace, mock_xp
+
+from tests.array_api.common import installed_array_modules
+
+with pytest.warns(HypothesisWarning):
+    mock_xps = make_strategies_namespace(mock_xp)
+
+# See README.md in regards to the HYPOTHESIS_TEST_ARRAY_API env variable
+test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default")
+name_to_entry_point = installed_array_modules()
+with warnings.catch_warnings():
+    # We ignore all warnings here as many array modules warn on import
+    warnings.simplefilter("ignore")
+    # We go through the steps described in README.md to define `params`, which
+    # contains the array module(s) to be ran against the test suite.
+    # Specifically `params` is a list of pytest parameters, with each parameter
+    # containing the array module and its respective strategies namespace.
+    if test_xp_option == "default":
+        try:
+            xp = name_to_entry_point["numpy"].load()
+            xps = make_strategies_namespace(xp)
+            params = [pytest.param(xp, xps, id="numpy")]
+        except KeyError:
+            params = [pytest.param(mock_xp, mock_xps, id="mock")]
+    elif test_xp_option == "all":
+        if len(name_to_entry_point) == 0:
+            raise ValueError(
+                "HYPOTHESIS_TEST_ARRAY_API='all', but no entry points where found"
+            )
+        params = [pytest.param(mock_xp, mock_xps, id="mock")]
+        for name, ep in name_to_entry_point.items():
+            xp = ep.load()
+            xps = make_strategies_namespace(xp)
+            params.append(pytest.param(xp, xps, id=name))
+    elif test_xp_option in name_to_entry_point.keys():
+        ep = name_to_entry_point[test_xp_option]
+        xp = ep.load()
+        xps = make_strategies_namespace(xp)
+        params = [pytest.param(xp, xps, id=test_xp_option)]
+    else:
+        try:
+            xp = import_module(test_xp_option)
+            xps = make_strategies_namespace(xp)
+            params = [pytest.param(xp, xps, id=test_xp_option)]
+        except ImportError as e:
+            raise ValueError(
+                f"HYPOTHESIS_TEST_ARRAY_API='{test_xp_option}' is not a valid "
+                "option ('default' or 'all'), name of an available entry point, "
+                "or a valid import path."
+            ) from e
+
+
+def pytest_generate_tests(metafunc):
+    if "xp" in metafunc.fixturenames and "xps" in metafunc.fixturenames:
+        metafunc.parametrize("xp, xps", params)
diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py
--- a/hypothesis-python/tests/array_api/test_argument_validation.py
+++ b/hypothesis-python/tests/array_api/test_argument_validation.py
@@ -12,76 +12,74 @@
 
 from hypothesis.errors import InvalidArgument
 
-from tests.array_api.common import xp, xps
 
-
-def e(a, **kwargs):
+def e(name, **kwargs):
     kw = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
-    return pytest.param(a, kwargs, id=f"{a.__name__}({kw})")
+    return pytest.param(name, kwargs, id=f"{name}({kw})")
 
 
 @pytest.mark.parametrize(
-    ("function", "kwargs"),
+    ("strat_name", "kwargs"),
     [
-        e(xps.arrays, dtype=1, shape=5),
-        e(xps.arrays, dtype=None, shape=5),
-        e(xps.arrays, dtype=xp.int8, shape=(0.5,)),
-        e(xps.arrays, dtype=xp.int8, shape=1, fill=3),
-        e(xps.arrays, dtype=xp.int8, shape=1, elements="not a strategy"),
-        e(xps.arrays, dtype=xp.int8, shape="not a shape or strategy"),
-        e(xps.array_shapes, min_side=2, max_side=1),
-        e(xps.array_shapes, min_dims=3, max_dims=2),
-        e(xps.array_shapes, min_dims=-1),
-        e(xps.array_shapes, min_side=-1),
-        e(xps.array_shapes, min_side="not an int"),
-        e(xps.array_shapes, max_side="not an int"),
-        e(xps.array_shapes, min_dims="not an int"),
-        e(xps.array_shapes, max_dims="not an int"),
-        e(xps.from_dtype, dtype=1),
-        e(xps.from_dtype, dtype=None),
-        e(xps.from_dtype, dtype=xp.int8, min_value="not an int"),
-        e(xps.from_dtype, dtype=xp.int8, max_value="not an int"),
-        e(xps.from_dtype, dtype=xp.float32, min_value="not a float"),
-        e(xps.from_dtype, dtype=xp.float32, max_value="not a float"),
-        e(xps.from_dtype, dtype=xp.int8, min_value=10, max_value=5),
-        e(xps.from_dtype, dtype=xp.float32, min_value=10, max_value=5),
-        e(xps.from_dtype, dtype=xp.int8, min_value=-999),
-        e(xps.from_dtype, dtype=xp.int8, max_value=-999),
-        e(xps.from_dtype, dtype=xp.int8, min_value=999),
-        e(xps.from_dtype, dtype=xp.int8, max_value=999),
-        e(xps.from_dtype, dtype=xp.uint8, min_value=-999),
-        e(xps.from_dtype, dtype=xp.uint8, max_value=-999),
-        e(xps.from_dtype, dtype=xp.uint8, min_value=999),
-        e(xps.from_dtype, dtype=xp.uint8, max_value=999),
-        e(xps.from_dtype, dtype=xp.float32, min_value=-4e38),
-        e(xps.from_dtype, dtype=xp.float32, max_value=-4e38),
-        e(xps.from_dtype, dtype=xp.float32, min_value=4e38),
-        e(xps.from_dtype, dtype=xp.float32, max_value=4e38),
-        e(xps.integer_dtypes, sizes=()),
-        e(xps.integer_dtypes, sizes=(3,)),
-        e(xps.unsigned_integer_dtypes, sizes=()),
-        e(xps.unsigned_integer_dtypes, sizes=(3,)),
-        e(xps.floating_dtypes, sizes=()),
-        e(xps.floating_dtypes, sizes=(3,)),
-        e(xps.valid_tuple_axes, ndim=-1),
-        e(xps.valid_tuple_axes, ndim=2, min_size=-1),
-        e(xps.valid_tuple_axes, ndim=2, min_size=3, max_size=10),
-        e(xps.valid_tuple_axes, ndim=2, min_size=2, max_size=1),
-        e(xps.valid_tuple_axes, ndim=2.0, min_size=2, max_size=1),
-        e(xps.valid_tuple_axes, ndim=2, min_size=1.0, max_size=2),
-        e(xps.valid_tuple_axes, ndim=2, min_size=1, max_size=2.0),
-        e(xps.valid_tuple_axes, ndim=2, min_size=1, max_size=3),
-        e(xps.broadcastable_shapes, shape="a"),
-        e(xps.broadcastable_shapes, shape=(2, 2), min_side="a"),
-        e(xps.broadcastable_shapes, shape=(2, 2), min_dims="a"),
-        e(xps.broadcastable_shapes, shape=(2, 2), max_side="a"),
-        e(xps.broadcastable_shapes, shape=(2, 2), max_dims="a"),
-        e(xps.broadcastable_shapes, shape=(2, 2), min_side=-1),
-        e(xps.broadcastable_shapes, shape=(2, 2), min_dims=-1),
-        e(xps.broadcastable_shapes, shape=(2, 2), min_side=1, max_side=0),
-        e(xps.broadcastable_shapes, shape=(2, 2), min_dims=1, max_dims=0),
-        e(
-            xps.broadcastable_shapes,  # max_side too small
+        e("arrays", dtype=1, shape=5),
+        e("arrays", dtype=None, shape=5),
+        e("arrays", dtype="int8", shape=(0.5,)),
+        e("arrays", dtype="int8", shape=1, fill=3),
+        e("arrays", dtype="int8", shape=1, elements="not a strategy"),
+        e("arrays", dtype="int8", shape="not a shape or strategy"),
+        e("array_shapes", min_side=2, max_side=1),
+        e("array_shapes", min_dims=3, max_dims=2),
+        e("array_shapes", min_dims=-1),
+        e("array_shapes", min_side=-1),
+        e("array_shapes", min_side="not an int"),
+        e("array_shapes", max_side="not an int"),
+        e("array_shapes", min_dims="not an int"),
+        e("array_shapes", max_dims="not an int"),
+        e("from_dtype", dtype=1),
+        e("from_dtype", dtype=None),
+        e("from_dtype", dtype="int8", min_value="not an int"),
+        e("from_dtype", dtype="int8", max_value="not an int"),
+        e("from_dtype", dtype="float32", min_value="not a float"),
+        e("from_dtype", dtype="float32", max_value="not a float"),
+        e("from_dtype", dtype="int8", min_value=10, max_value=5),
+        e("from_dtype", dtype="float32", min_value=10, max_value=5),
+        e("from_dtype", dtype="int8", min_value=-999),
+        e("from_dtype", dtype="int8", max_value=-999),
+        e("from_dtype", dtype="int8", min_value=999),
+        e("from_dtype", dtype="int8", max_value=999),
+        e("from_dtype", dtype="uint8", min_value=-999),
+        e("from_dtype", dtype="uint8", max_value=-999),
+        e("from_dtype", dtype="uint8", min_value=999),
+        e("from_dtype", dtype="uint8", max_value=999),
+        e("from_dtype", dtype="float32", min_value=-4e38),
+        e("from_dtype", dtype="float32", max_value=-4e38),
+        e("from_dtype", dtype="float32", min_value=4e38),
+        e("from_dtype", dtype="float32", max_value=4e38),
+        e("integer_dtypes", sizes=()),
+        e("integer_dtypes", sizes=(3,)),
+        e("unsigned_integer_dtypes", sizes=()),
+        e("unsigned_integer_dtypes", sizes=(3,)),
+        e("floating_dtypes", sizes=()),
+        e("floating_dtypes", sizes=(3,)),
+        e("valid_tuple_axes", ndim=-1),
+        e("valid_tuple_axes", ndim=2, min_size=-1),
+        e("valid_tuple_axes", ndim=2, min_size=3, max_size=10),
+        e("valid_tuple_axes", ndim=2, min_size=2, max_size=1),
+        e("valid_tuple_axes", ndim=2.0, min_size=2, max_size=1),
+        e("valid_tuple_axes", ndim=2, min_size=1.0, max_size=2),
+        e("valid_tuple_axes", ndim=2, min_size=1, max_size=2.0),
+        e("valid_tuple_axes", ndim=2, min_size=1, max_size=3),
+        e("broadcastable_shapes", shape="a"),
+        e("broadcastable_shapes", shape=(2, 2), min_side="a"),
+        e("broadcastable_shapes", shape=(2, 2), min_dims="a"),
+        e("broadcastable_shapes", shape=(2, 2), max_side="a"),
+        e("broadcastable_shapes", shape=(2, 2), max_dims="a"),
+        e("broadcastable_shapes", shape=(2, 2), min_side=-1),
+        e("broadcastable_shapes", shape=(2, 2), min_dims=-1),
+        e("broadcastable_shapes", shape=(2, 2), min_side=1, max_side=0),
+        e("broadcastable_shapes", shape=(2, 2), min_dims=1, max_dims=0),
+        e(
+            "broadcastable_shapes",  # max_side too small
             shape=(5, 1),
             min_dims=2,
             max_dims=4,
@@ -89,7 +87,7 @@ def e(a, **kwargs):
             max_side=3,
         ),
         e(
-            xps.broadcastable_shapes,  # min_side too large
+            "broadcastable_shapes",  # min_side too large
             shape=(0, 1),
             min_dims=2,
             max_dims=4,
@@ -97,7 +95,7 @@ def e(a, **kwargs):
             max_side=3,
         ),
         e(
-            xps.broadcastable_shapes,  # default max_dims unsatisfiable
+            "broadcastable_shapes",  # default max_dims unsatisfiable
             shape=(5, 3, 2, 1),
             min_dims=3,
             max_dims=None,
@@ -105,60 +103,60 @@ def e(a, **kwargs):
             max_side=3,
         ),
         e(
-            xps.broadcastable_shapes,  # default max_dims unsatisfiable
+            "broadcastable_shapes",  # default max_dims unsatisfiable
             shape=(0, 3, 2, 1),
             min_dims=3,
             max_dims=None,
             min_side=2,
             max_side=3,
         ),
-        e(xps.mutually_broadcastable_shapes, num_shapes=0),
-        e(xps.mutually_broadcastable_shapes, num_shapes="a"),
-        e(xps.mutually_broadcastable_shapes, num_shapes=2, base_shape="a"),
+        e("mutually_broadcastable_shapes", num_shapes=0),
+        e("mutually_broadcastable_shapes", num_shapes="a"),
+        e("mutually_broadcastable_shapes", num_shapes=2, base_shape="a"),
         e(
-            xps.mutually_broadcastable_shapes,  # min_side is invalid type
+            "mutually_broadcastable_shapes",  # min_side is invalid type
             num_shapes=2,
             min_side="a",
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # min_dims is invalid type
+            "mutually_broadcastable_shapes",  # min_dims is invalid type
             num_shapes=2,
             min_dims="a",
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # max_side is invalid type
+            "mutually_broadcastable_shapes",  # max_side is invalid type
             num_shapes=2,
             max_side="a",
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # max_side is invalid type
+            "mutually_broadcastable_shapes",  # max_side is invalid type
             num_shapes=2,
             max_dims="a",
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # min_side is out of domain
+            "mutually_broadcastable_shapes",  # min_side is out of domain
             num_shapes=2,
             min_side=-1,
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # min_dims is out of domain
+            "mutually_broadcastable_shapes",  # min_dims is out of domain
             num_shapes=2,
             min_dims=-1,
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # max_side < min_side
+            "mutually_broadcastable_shapes",  # max_side < min_side
             num_shapes=2,
             min_side=1,
             max_side=0,
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # max_dims < min_dims
+            "mutually_broadcastable_shapes",  # max_dims < min_dims
             num_shapes=2,
             min_dims=1,
             max_dims=0,
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # max_side too small
+            "mutually_broadcastable_shapes",  # max_side too small
             num_shapes=2,
             base_shape=(5, 1),
             min_dims=2,
@@ -167,7 +165,7 @@ def e(a, **kwargs):
             max_side=3,
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # min_side too large
+            "mutually_broadcastable_shapes",  # min_side too large
             num_shapes=2,
             base_shape=(0, 1),
             min_dims=2,
@@ -176,7 +174,7 @@ def e(a, **kwargs):
             max_side=3,
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # user-specified max_dims unsatisfiable
+            "mutually_broadcastable_shapes",  # user-specified max_dims unsatisfiable
             num_shapes=1,
             base_shape=(5, 3, 2, 1),
             min_dims=3,
@@ -185,7 +183,7 @@ def e(a, **kwargs):
             max_side=3,
         ),
         e(
-            xps.mutually_broadcastable_shapes,  # user-specified max_dims unsatisfiable
+            "mutually_broadcastable_shapes",  # user-specified max_dims unsatisfiable
             num_shapes=2,
             base_shape=(0, 3, 2, 1),
             min_dims=3,
@@ -193,21 +191,23 @@ def e(a, **kwargs):
             min_side=2,
             max_side=3,
         ),
-        e(xps.indices, shape=0),
-        e(xps.indices, shape=("1", "2")),
-        e(xps.indices, shape=(0, -1)),
-        e(xps.indices, shape=(0, 0), allow_ellipsis=None),
-        e(xps.indices, shape=(0, 0), min_dims=-1),
-        e(xps.indices, shape=(0, 0), min_dims=1.0),
-        e(xps.indices, shape=(0, 0), max_dims=-1),
-        e(xps.indices, shape=(0, 0), max_dims=1.0),
-        e(xps.indices, shape=(0, 0), min_dims=2, max_dims=1),
-        e(xps.indices, shape=5, min_dims=0),
-        e(xps.indices, shape=(5,), min_dims=2),
-        e(xps.indices, shape=(5,), max_dims=2),
+        e("indices", shape=0),
+        e("indices", shape=("1", "2")),
+        e("indices", shape=(0, -1)),
+        e("indices", shape=(0, 0), allow_ellipsis=None),
+        e("indices", shape=(0, 0), min_dims=-1),
+        e("indices", shape=(0, 0), min_dims=1.0),
+        e("indices", shape=(0, 0), max_dims=-1),
+        e("indices", shape=(0, 0), max_dims=1.0),
+        e("indices", shape=(0, 0), min_dims=2, max_dims=1),
+        e("indices", shape=5, min_dims=0),
+        e("indices", shape=(5,), min_dims=2),
+        e("indices", shape=(5,), max_dims=2),
     ],
 )
-def test_raise_invalid_argument(function, kwargs):
+def test_raise_invalid_argument(xp, xps, strat_name, kwargs):
     """Strategies raise helpful error with invalid arguments."""
+    strat_func = getattr(xps, strat_name)
+    strat = strat_func(**kwargs)
     with pytest.raises(InvalidArgument):
-        function(**kwargs).example()
+        strat.example()
diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py
--- a/hypothesis-python/tests/array_api/test_arrays.py
+++ b/hypothesis-python/tests/array_api/test_arrays.py
@@ -10,145 +10,137 @@
 
 import pytest
 
-from hypothesis import assume, given, strategies as st
+from hypothesis import given, strategies as st
 from hypothesis.errors import InvalidArgument
 from hypothesis.extra.array_api import DTYPE_NAMES, NUMERIC_NAMES
 from hypothesis.internal.floats import width_smallest_normals
 
-from tests.array_api.common import COMPLIANT_XP, WIDTHS_FTZ, xp, xps
-from tests.common.debug import find_any, minimal
-from tests.common.utils import fails_with, flaky
+from tests.array_api.common import flushes_to_zero
+from tests.common.debug import assert_all_examples, find_any, minimal
+from tests.common.utils import flaky
 
-needs_xp_unique_values = pytest.mark.skipif(
-    not hasattr(xp, "unique_values"), reason="optional API"
-)
-
-
-# xp.unique_value() should return distinct NaNs - if not, tests that (rightly)
-# assume such behaviour will likely fail. This mark namely addresses mocking the
-# array module with NumPy 1.21, which treats NaNs as not distinct.
-# See https://mail.python.org/pipermail/numpy-discussion/2021-August/081995.html
-two_nans = xp.asarray([float("nan"), float("nan")])
-assumes_distinct_nans = pytest.mark.xfail(
-    hasattr(xp, "unique_values") and xp.unique_values(two_nans).size != 2,
-    reason="NaNs not distinct",
-)
 
+def skip_on_missing_unique_values(xp):
+    if not hasattr(xp, "unique_values"):
+        pytest.mark.skip("xp.unique_values() is not required to exist")
 
-def assert_array_namespace(x):
-    """Check array has __array_namespace__() and it returns the correct module.
 
-    This check is skipped if a mock array module is being used.
+def xfail_on_indistinct_nans(xp):
+    """
+    xp.unique_value() should return distinct NaNs - if not, tests that (rightly)
+    assume such behaviour will likely fail. For example, NumPy 1.22 treats NaNs
+    as indistinct, so tests that use this function will be marked as xfail.
+    See https://mail.python.org/pipermail/numpy-discussion/2021-August/081995.html
     """
-    if COMPLIANT_XP:
-        assert x.__array_namespace__() is xp
+    skip_on_missing_unique_values(xp)
+    two_nans = xp.asarray([float("nan"), float("nan")])
+    if xp.unique_values(two_nans).size != 2:
+        pytest.xfail("NaNs not distinct")
 
 
-@given(xps.scalar_dtypes(), st.data())
-def test_draw_arrays_from_dtype(dtype, data):
+@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+def test_draw_arrays_from_dtype(xp, xps, dtype_name):
     """Draw arrays from dtypes."""
-    x = data.draw(xps.arrays(dtype, ()))
-    assert x.dtype == dtype
-    assert_array_namespace(x)
+    dtype = getattr(xp, dtype_name)
+    assert_all_examples(xps.arrays(dtype, ()), lambda x: x.dtype == dtype)
 
 
-@given(st.sampled_from(DTYPE_NAMES), st.data())
-def test_draw_arrays_from_scalar_names(name, data):
+@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+def test_draw_arrays_from_scalar_names(xp, xps, dtype_name):
     """Draw arrays from dtype names."""
-    x = data.draw(xps.arrays(name, ()))
-    assert x.dtype == getattr(xp, name)
-    assert_array_namespace(x)
+    dtype = getattr(xp, dtype_name)
+    assert_all_examples(xps.arrays(dtype_name, ()), lambda x: x.dtype == dtype)
 
 
-@given(xps.array_shapes(), st.data())
-def test_draw_arrays_from_shapes(shape, data):
+@given(data=st.data())
+def test_draw_arrays_from_shapes(xp, xps, data):
     """Draw arrays from shapes."""
+    shape = data.draw(xps.array_shapes())
     x = data.draw(xps.arrays(xp.int8, shape))
     assert x.ndim == len(shape)
     assert x.shape == shape
-    assert_array_namespace(x)
 
 
-@given(st.integers(0, 10), st.data())
-def test_draw_arrays_from_int_shapes(size, data):
+@given(data=st.data())
+def test_draw_arrays_from_int_shapes(xp, xps, data):
     """Draw arrays from integers as shapes."""
+    size = data.draw(st.integers(0, 10))
     x = data.draw(xps.arrays(xp.int8, size))
     assert x.shape == (size,)
-    assert_array_namespace(x)
 
 
 @pytest.mark.parametrize(
-    "strat",
+    "strat_name",
     [
-        xps.scalar_dtypes(),
-        xps.boolean_dtypes(),
-        xps.integer_dtypes(),
-        xps.unsigned_integer_dtypes(),
-        xps.floating_dtypes(),
+        "scalar_dtypes",
+        "boolean_dtypes",
+        "integer_dtypes",
+        "unsigned_integer_dtypes",
+        "floating_dtypes",
     ],
 )
-@given(st.data())
-def test_draw_arrays_from_dtype_strategies(strat, data):
+def test_draw_arrays_from_dtype_strategies(xp, xps, strat_name):
     """Draw arrays from dtype strategies."""
-    x = data.draw(xps.arrays(strat, ()))
-    assert_array_namespace(x)
+    strat_func = getattr(xps, strat_name)
+    strat = strat_func()
+    find_any(xps.arrays(strat, ()))
 
 
-@given(st.lists(st.sampled_from(DTYPE_NAMES), min_size=1, unique=True), st.data())
-def test_draw_arrays_from_dtype_name_strategies(names, data):
+@given(
+    strat=st.lists(st.sampled_from(DTYPE_NAMES), min_size=1, unique=True).flatmap(
+        st.sampled_from
+    )
+)
+def test_draw_arrays_from_dtype_name_strategies(xp, xps, strat):
     """Draw arrays from dtype name strategies."""
-    names_strategy = st.sampled_from(names)
-    x = data.draw(xps.arrays(names_strategy, ()))
-    assert_array_namespace(x)
+    find_any(xps.arrays(strat, ()))
 
 
-@given(xps.arrays(xp.int8, xps.array_shapes()))
-def test_generate_arrays_from_shapes_strategy(x):
+def test_generate_arrays_from_shapes_strategy(xp, xps):
     """Generate arrays from shapes strategy."""
-    assert_array_namespace(x)
+    find_any(xps.arrays(xp.int8, xps.array_shapes()))
 
 
-@given(xps.arrays(xp.int8, st.integers(0, 100)))
-def test_generate_arrays_from_integers_strategy_as_shape(x):
+def test_generate_arrays_from_integers_strategy_as_shape(xp, xps):
     """Generate arrays from integers strategy as shapes strategy."""
-    assert_array_namespace(x)
+    find_any(xps.arrays(xp.int8, st.integers(0, 100)))
 
 
-@given(xps.arrays(xp.int8, ()))
-def test_generate_arrays_from_zero_dimensions(x):
+def test_generate_arrays_from_zero_dimensions(xp, xps):
     """Generate arrays from empty shape."""
-    assert x.shape == ()
-    assert_array_namespace(x)
+    assert_all_examples(xps.arrays(xp.int8, ()), lambda x: x.shape == ())
 
 
-@given(xps.arrays(xp.int8, (1, 0, 1)))
-def test_handle_zero_dimensions(x):
-    """Generate arrays from empty shape."""
-    assert x.shape == (1, 0, 1)
-    assert_array_namespace(x)
+@given(data=st.data())
+def test_generate_arrays_from_zero_sided_shapes(xp, xps, data):
+    """Generate arrays from shapes with at least one 0-sized dimension."""
+    shape = data.draw(xps.array_shapes(min_side=0).filter(lambda s: 0 in s))
+    assert_all_examples(xps.arrays(xp.int8, shape), lambda x: x.shape == shape)
 
 
-@given(xps.arrays(xp.uint32, (5, 5)))
-def test_generate_arrays_from_unsigned_ints(x):
+def test_generate_arrays_from_unsigned_ints(xp, xps):
     """Generate arrays from unsigned integer dtype."""
-    assert xp.all(x >= 0)
-    assert_array_namespace(x)
+    assert_all_examples(xps.arrays(xp.uint32, (5, 5)), lambda x: xp.all(x >= 0))
+    # Ensure we're not just picking non-negative signed integers
+    signed_max = xp.iinfo(xp.int32).max
+    find_any(xps.arrays(xp.uint32, (5, 5)), lambda x: xp.any(x > signed_max))
 
 
-@given(
-    xps.arrays(
-        dtype=xp.uint8,
-        shape=(5, 5),
-        elements=xps.from_dtype(xp.uint8).map(lambda e: xp.asarray(e, dtype=xp.uint8)),
-    )
-)
-def test_generate_arrays_from_0d_arrays(x):
+def test_generate_arrays_from_0d_arrays(xp, xps):
     """Generate arrays from 0d array elements."""
-    assert x.shape == (5, 5)
-    assert_array_namespace(x)
+    assert_all_examples(
+        xps.arrays(
+            dtype=xp.uint8,
+            shape=(5, 5),
+            elements=xps.from_dtype(xp.uint8).map(
+                lambda e: xp.asarray(e, dtype=xp.uint8)
+            ),
+        ),
+        lambda x: x.shape == (5, 5),
+    )
 
 
-def test_minimize_arrays_with_default_dtype_shape_strategies():
+def test_minimize_arrays_with_default_dtype_shape_strategies(xp, xps):
     """Strategy with default scalar_dtypes and array_shapes strategies minimize
     to a boolean 1-dimensional array of size 1."""
     smallest = minimal(xps.arrays(xps.scalar_dtypes(), xps.array_shapes()))
@@ -157,7 +149,7 @@ def test_minimize_arrays_with_default_dtype_shape_strategies():
     assert not xp.any(smallest)
 
 
-def test_minimize_arrays_with_0d_shape_strategy():
+def test_minimize_arrays_with_0d_shape_strategy(xp, xps):
     """Strategy with shape strategy that can generate empty tuples minimizes to
     0d arrays."""
     smallest = minimal(xps.arrays(xp.int8, xps.array_shapes(min_dims=0)))
@@ -165,16 +157,17 @@ def test_minimize_arrays_with_0d_shape_strategy():
 
 
 @pytest.mark.parametrize("dtype", NUMERIC_NAMES)
-def test_minimizes_numeric_arrays(dtype):
+def test_minimizes_numeric_arrays(xp, xps, dtype):
     """Strategies with numeric dtypes minimize to zero-filled arrays."""
     smallest = minimal(xps.arrays(dtype, (2, 2)))
     assert xp.all(smallest == 0)
 
 
-@pytest.mark.skipif(not hasattr(xp, "nonzero"), reason="optional API")
-def test_minimize_large_uint_arrays():
+def test_minimize_large_uint_arrays(xp, xps):
     """Strategy with uint dtype and largely sized shape minimizes to a good
     example."""
+    if not hasattr(xp, "nonzero"):
+        pytest.skip("optional API")
     smallest = minimal(
         xps.arrays(xp.uint8, 100),
         lambda x: xp.any(x) and not xp.all(x),
@@ -187,7 +180,7 @@ def test_minimize_large_uint_arrays():
 
 @pytest.mark.filterwarnings("ignore::RuntimeWarning")
 @flaky(max_runs=50, min_passes=1)
-def test_minimize_float_arrays():
+def test_minimize_float_arrays(xp, xps):
     """Strategy with float dtype minimizes to a good example.
 
     We filter runtime warnings and expect flaky array generation for
@@ -198,57 +191,65 @@ def test_minimize_float_arrays():
     assert xp.sum(smallest) in (1, 50)
 
 
-def test_minimizes_to_fill():
+def test_minimizes_to_fill(xp, xps):
     """Strategy with single fill value minimizes to arrays only containing said
     fill value."""
     smallest = minimal(xps.arrays(xp.float32, 10, fill=st.just(3.0)))
     assert xp.all(smallest == 3.0)
 
 
-@needs_xp_unique_values
-@given(xps.arrays(xp.int8, st.integers(0, 20), unique=True))
-def test_generate_unique_arrays(x):
+def test_generate_unique_arrays(xp, xps):
     """Generates unique arrays."""
-    assert xp.unique_values(x).size == x.size
+    skip_on_missing_unique_values(xp)
+    assert_all_examples(
+        xps.arrays(xp.int8, st.integers(0, 20), unique=True),
+        lambda x: xp.unique_values(x).size == x.size,
+    )
 
 
-@fails_with(InvalidArgument)
-@given(xps.arrays(xp.int8, 10, elements=st.integers(0, 5), unique=True))
-def test_cannot_draw_unique_arrays_with_too_small_elements(_):
+def test_cannot_draw_unique_arrays_with_too_small_elements(xp, xps):
     """Unique strategy with elements strategy range smaller than its size raises
     helpful error."""
+    with pytest.raises(InvalidArgument):
+        xps.arrays(xp.int8, 10, elements=st.integers(0, 5), unique=True).example()
 
 
-@fails_with(InvalidArgument)
-@given(xps.arrays(xp.int8, 10, fill=st.just("not a castable value")))
-def test_cannot_fill_arrays_with_non_castable_value():
+def test_cannot_fill_arrays_with_non_castable_value(xp, xps):
     """Strategy with fill not castable to dtype raises helpful error."""
+    with pytest.raises(InvalidArgument):
+        xps.arrays(xp.int8, 10, fill=st.just("not a castable value")).example()
 
 
-@given(
-    xps.arrays(
-        dtype=xp.float32,
-        shape=st.integers(0, 20),
-        elements=st.just(0.0),
-        fill=st.just(xp.nan),
-        unique=True,
-    )
-)
-def test_generate_unique_arrays_with_high_collision_elements(x):
+def test_generate_unique_arrays_with_high_collision_elements(xp, xps):
     """Generates unique arrays with just elements of 0.0 and NaN fill."""
-    zero_mask = x == 0.0
-    assert xp.sum(xp.astype(zero_mask, xp.uint8)) <= 1
+
+    @given(
+        xps.arrays(
+            dtype=xp.float32,
+            shape=st.integers(0, 20),
+            elements=st.just(0.0),
+            fill=st.just(xp.nan),
+            unique=True,
+        )
+    )
+    def test(x):
+        zero_mask = x == 0.0
+        assert xp.sum(xp.astype(zero_mask, xp.uint8)) <= 1
+
+    test()
 
 
-@needs_xp_unique_values
-@given(xps.arrays(xp.int8, (4,), elements=st.integers(0, 3), unique=True))
-def test_generate_unique_arrays_using_all_elements(x):
+def test_generate_unique_arrays_using_all_elements(xp, xps):
     """Unique strategy with elements strategy range equal to its size will only
     generate arrays with one of each possible element."""
-    assert xp.unique_values(x).size == x.size
+    skip_on_missing_unique_values(xp)
+    assert_all_examples(
+        xps.arrays(xp.int8, (4,), elements=st.integers(0, 3), unique=True),
+        lambda x: xp.unique_values(x).size == x.size,
+    )
 
 
-def test_may_fill_unique_arrays_with_nan():
+def test_may_fill_unique_arrays_with_nan(xp, xps):
     """Unique strategy with NaN fill can generate arrays holding NaNs."""
     find_any(
         xps.arrays(
@@ -262,18 +263,17 @@ def test_may_fill_unique_arrays_with_nan():
     )
 
 
-@fails_with(InvalidArgument)
-@given(
-    xps.arrays(
-        dtype=xp.float32,
-        shape=10,
-        elements={"allow_nan": False},
-        unique=True,
-        fill=st.just(0.0),
-    )
-)
-def test_may_not_fill_unique_array_with_non_nan(_):
+def test_may_not_fill_unique_array_with_non_nan(xp, xps):
     """Unique strategy with just fill elements of 0.0 raises helpful error."""
+    with pytest.raises(InvalidArgument):
+        strat = xps.arrays(
+            dtype=xp.float32,
+            shape=10,
+            elements={"allow_nan": False},
+            unique=True,
+            fill=st.just(0.0),
+        )
+        strat.example()
 
 
 @pytest.mark.parametrize(
@@ -283,100 +283,94 @@ def test_may_not_fill_unique_array_with_non_nan(_):
         {"elements": st.nothing(), "fill": st.just(300)},
     ],
 )
-@fails_with(InvalidArgument)
-@given(st.data())
-def test_may_not_use_overflowing_integers(kwargs, data):
+def test_may_not_use_overflowing_integers(xp, xps, kwargs):
     """Strategy with elements strategy range outside the dtype's bounds raises
     helpful error."""
-    strat = xps.arrays(dtype=xp.int8, shape=1, **kwargs)
-    data.draw(strat)
+    with pytest.raises(InvalidArgument):
+        xps.arrays(dtype=xp.int8, shape=1, **kwargs).example()
 
 
 @pytest.mark.parametrize("fill", [False, True])
 @pytest.mark.parametrize(
     "dtype, strat",
     [
-        (xp.float32, st.floats(min_value=10**40, allow_infinity=False)),
-        (xp.float64, st.floats(min_value=10**40, allow_infinity=False)),
+        ("float32", st.floats(min_value=10**40, allow_infinity=False)),
+        ("float64", st.floats(min_value=10**40, allow_infinity=False)),
     ],
 )
-@fails_with(InvalidArgument)
-@given(st.data())
-def test_may_not_use_unrepresentable_elements(fill, dtype, strat, data):
+def test_may_not_use_unrepresentable_elements(xp, xps, fill, dtype, strat):
     """Strategy with elements not representable by the dtype raises helpful error."""
     if fill:
         kw = {"elements": st.nothing(), "fill": strat}
     else:
         kw = {"elements": strat}
-    strat = xps.arrays(dtype=dtype, shape=1, **kw)
-    data.draw(strat)
+    with pytest.raises(InvalidArgument):
+        xps.arrays(dtype=dtype, shape=1, **kw).example()
 
 
-@given(
-    xps.arrays(dtype=xp.float32, shape=10, elements={"min_value": 0, "max_value": 1})
-)
-def test_floats_can_be_constrained(x):
+def test_floats_can_be_constrained(xp, xps):
     """Strategy with float dtype and specified elements strategy range
     (inclusive) generates arrays with elements inside said range."""
-    assert xp.all(x >= 0)
-    assert xp.all(x <= 1)
+    assert_all_examples(
+        xps.arrays(
+            dtype=xp.float32, shape=10, elements={"min_value": 0, "max_value": 1}
+        ),
+        lambda x: xp.all(x >= 0) and xp.all(x <= 1),
+    )
 
 
-@given(
-    xps.arrays(
-        dtype=xp.float32,
-        shape=10,
-        elements={
-            "min_value": 0,
-            "max_value": 1,
-            "exclude_min": True,
-            "exclude_max": True,
-        },
-    )
-)
-def test_floats_can_be_constrained_excluding_endpoints(x):
+def test_floats_can_be_constrained_excluding_endpoints(xp, xps):
     """Strategy with float dtype and specified elements strategy range
     (exclusive) generates arrays with elements inside said range."""
-    assert xp.all(x > 0)
-    assert xp.all(x < 1)
+    assert_all_examples(
+        xps.arrays(
+            dtype=xp.float32,
+            shape=10,
+            elements={
+                "min_value": 0,
+                "max_value": 1,
+                "exclude_min": True,
+                "exclude_max": True,
+            },
+        ),
+        lambda x: xp.all(x > 0) and xp.all(x < 1),
+    )
 
 
-@needs_xp_unique_values
-@assumes_distinct_nans
-@given(
-    xps.arrays(
-        dtype=xp.float32,
-        elements={"allow_nan": False},
-        shape=10,
-        unique=True,
-        fill=st.just(xp.nan),
-    )
-)
-def test_is_still_unique_with_nan_fill(x):
+def test_is_still_unique_with_nan_fill(xp, xps):
     """Unique strategy with NaN fill generates unique arrays."""
-    assert xp.unique_values(x).size == x.size
+    skip_on_missing_unique_values(xp)
+    xfail_on_indistinct_nans(xp)
+    assert_all_examples(
+        xps.arrays(
+            dtype=xp.float32,
+            elements={"allow_nan": False},
+            shape=10,
+            unique=True,
+            fill=st.just(xp.nan),
+        ),
+        lambda x: xp.unique_values(x).size == x.size,
+    )
 
 
-@needs_xp_unique_values
-@assumes_distinct_nans
-@given(
-    xps.arrays(
-        dtype=xp.float32,
-        shape=10,
-        unique=True,
-        elements=st.integers(1, 9),
-        fill=st.just(xp.nan),
-    )
-)
-def test_unique_array_with_fill_can_use_all_elements(x):
+def test_unique_array_with_fill_can_use_all_elements(xp, xps):
     """Unique strategy with elements range equivalent to its size and NaN fill
     can generate arrays with all possible values."""
-    assume(xp.unique_values(x).size == x.size)
+    skip_on_missing_unique_values(xp)
+    xfail_on_indistinct_nans(xp)
+    find_any(
+        xps.arrays(
+            dtype=xp.float32,
+            shape=10,
+            unique=True,
+            elements=st.integers(1, 9),
+            fill=st.just(xp.nan),
+        ),
+        lambda x: xp.unique_values(x).size == x.size,
+    )
 
 
-@needs_xp_unique_values
-@given(xps.arrays(dtype=xp.uint8, shape=25, unique=True, fill=st.nothing()))
-def test_generate_unique_arrays_without_fill(x):
+def test_generate_unique_arrays_without_fill(xp, xps):
     """Generate arrays from unique strategy with no fill.
 
     Covers the collision-related branches for fully dense unique arrays.
@@ -384,23 +378,28 @@ def test_generate_unique_arrays_without_fill(x):
     colisions thanks to the birthday paradox, but finding unique values should
     still be easy.
     """
-    assume(xp.unique_values(x).size == x.size)
+    skip_on_missing_unique_values(xp)
+    assert_all_examples(
+        xps.arrays(dtype=xp.uint8, shape=25, unique=True, fill=st.nothing()),
+        lambda x: xp.unique_values(x).size == x.size,
+    )
 
 
-@needs_xp_unique_values
-@given(xps.arrays(dtype=xp.int8, shape=255, unique=True))
-def test_efficiently_generate_unique_arrays_using_all_elements(x):
+def test_efficiently_generate_unique_arrays_using_all_elements(xp, xps):
     """Unique strategy with elements strategy range equivalent to its size
     generates arrays with all possible values. Generation is not too slow.
 
     Avoids the birthday paradox with UniqueSampledListStrategy.
     """
-    assert xp.unique_values(x).size == x.size
+    skip_on_missing_unique_values(xp)
+    assert_all_examples(
+        xps.arrays(dtype=xp.int8, shape=255, unique=True),
+        lambda x: xp.unique_values(x).size == x.size,
+    )
 
 
-@needs_xp_unique_values
 @given(st.data(), st.integers(-100, 100), st.integers(1, 100))
-def test_array_element_rewriting(data, start, size):
+def test_array_element_rewriting(xp, xps, data, start, size):
     """Unique strategy generates arrays with expected elements."""
     x = data.draw(
         xps.arrays(
@@ -415,17 +414,18 @@ def test_array_element_rewriting(data, start, size):
     assert xp.all(x_set == x_set_expect)
 
 
-@given(xps.arrays(xp.bool, (), fill=st.nothing()))
-def test_generate_0d_arrays_with_no_fill(x):
+def test_generate_0d_arrays_with_no_fill(xp, xps):
     """Generate arrays with zero-dimensions and no fill."""
-    assert x.dtype == xp.bool
-    assert x.shape == ()
+    assert_all_examples(
+        xps.arrays(xp.bool, (), fill=st.nothing()),
+        lambda x: x.dtype == xp.bool and x.shape == (),
+    )
 
 
-@pytest.mark.parametrize("dtype", [xp.float32, xp.float64])
+@pytest.mark.parametrize("dtype", ["float32", "float64"])
 @pytest.mark.parametrize("low", [-2.0, -1.0, 0.0, 1.0])
 @given(st.data())
-def test_excluded_min_in_float_arrays(dtype, low, data):
+def test_excluded_min_in_float_arrays(xp, xps, dtype, low, data):
     """Strategy with elements strategy excluding min does not generate arrays
     with elements less or equal to said min."""
     strat = xps.arrays(
@@ -442,49 +442,56 @@ def test_excluded_min_in_float_arrays(dtype, low, data):
 
 
 @st.composite
-def distinct_integers(draw):
-    used = draw(st.shared(st.builds(set), key="distinct_integers.used"))
-    i = draw(st.integers(0, 2**64 - 1).filter(lambda x: x not in used))
+def distinct_int64_integers(draw):
+    used = draw(st.shared(st.builds(set), key="distinct_int64_integers.used"))
+    int64_max = 2**63
+    i = draw(st.integers(1 - int64_max, int64_max).filter(lambda x: x not in used))
     used.add(i)
     return i
 
 
-@needs_xp_unique_values
-@given(xps.arrays(xp.uint64, 10, elements=distinct_integers()))
-def test_does_not_reuse_distinct_integers(x):
+def test_does_not_reuse_distinct_integers(xp, xps):
     """Strategy with distinct integer elements strategy generates arrays with
     distinct values."""
-    assert xp.unique_values(x).size == x.size
+    skip_on_missing_unique_values(xp)
+    assert_all_examples(
+        xps.arrays(xp.int64, 10, elements=distinct_int64_integers()),
+        lambda x: xp.unique_values(x).size == x.size,
+    )
 
 
-@needs_xp_unique_values
-def test_may_reuse_distinct_integers_if_asked():
+def test_may_reuse_distinct_integers_if_asked(xp, xps):
     """Strategy with shared elements and fill strategies of distinct integers
     may generate arrays with non-distinct values."""
+    skip_on_missing_unique_values(xp)
     find_any(
         xps.arrays(
-            xp.uint64, 10, elements=distinct_integers(), fill=distinct_integers()
+            xp.int64,
+            10,
+            elements=distinct_int64_integers(),
+            fill=distinct_int64_integers(),
         ),
         lambda x: xp.unique_values(x).size < x.size,
     )
 
 
-@pytest.mark.skipif(
-    not WIDTHS_FTZ[32], reason="Subnormals are valid for non-FTZ builds"
-)
-def test_cannot_draw_subnormals_for_ftz_float32():
-    """For FTZ builds of array modules, strategy with subnormal elements
-    strategy raises helpful error."""
-    strat = xps.arrays(
-        xp.float32,
-        10,
-        elements={
-            "min_value": 0.0,
-            "max_value": width_smallest_normals[32],
-            "exclude_min": True,
-            "exclude_max": True,
-            "allow_subnormal": True,
-        },
-    )
-    with pytest.raises(InvalidArgument, match="Generated subnormal float"):
+def test_subnormal_elements_validation(xp, xps):
+    """Strategy with subnormal elements strategy is correctly validated.
+
+    For FTZ builds of array modules, a helpful error should raise. Conversely,
+    for builds of array modules which support subnormals, the strategy should
+    generate arrays without raising.
+    """
+    elements = {
+        "min_value": 0.0,
+        "max_value": width_smallest_normals[32],
+        "exclude_min": True,
+        "exclude_max": True,
+        "allow_subnormal": True,
+    }
+    strat = xps.arrays(xp.float32, 10, elements=elements)
+    if flushes_to_zero(xp, width=32):
+        with pytest.raises(InvalidArgument, match="Generated subnormal float"):
+            strat.example()
+    else:
         strat.example()
diff --git a/hypothesis-python/tests/array_api/test_from_dtype.py b/hypothesis-python/tests/array_api/test_from_dtype.py
--- a/hypothesis-python/tests/array_api/test_from_dtype.py
+++ b/hypothesis-python/tests/array_api/test_from_dtype.py
@@ -12,124 +12,101 @@
 
 import pytest
 
-from hypothesis import given, strategies as st
 from hypothesis.extra.array_api import DTYPE_NAMES, find_castable_builtin_for_dtype
 from hypothesis.internal.floats import width_smallest_normals
 
-from tests.array_api.common import WIDTHS_FTZ, xp, xps
-from tests.common.debug import assert_no_examples, find_any, minimal
+from tests.array_api.common import flushes_to_zero
+from tests.common.debug import (
+    assert_all_examples,
+    assert_no_examples,
+    find_any,
+    minimal,
+)
 
 
-@given(xps.scalar_dtypes())
-def test_strategies_have_reusable_values(dtype):
+@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+def test_strategies_have_reusable_values(xp, xps, dtype_name):
     """Inferred strategies have reusable values."""
-    strat = xps.from_dtype(dtype)
+    strat = xps.from_dtype(dtype_name)
     assert strat.has_reusable_values
 
 
-DTYPES = [getattr(xp, name) for name in DTYPE_NAMES]
-
-
-@pytest.mark.parametrize("dtype", DTYPES)
-def test_produces_castable_instances_from_dtype(dtype):
+@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+def test_produces_castable_instances_from_dtype(xp, xps, dtype_name):
     """Strategies inferred by dtype generate values of a builtin type castable
     to the dtype."""
+    dtype = getattr(xp, dtype_name)
     builtin = find_castable_builtin_for_dtype(xp, dtype)
-
-    @given(xps.from_dtype(dtype))
-    def test_is_builtin(value):
-        assert isinstance(value, builtin)
-
-    test_is_builtin()
+    assert_all_examples(xps.from_dtype(dtype), lambda v: isinstance(v, builtin))
 
 
-@pytest.mark.parametrize("name", DTYPE_NAMES)
-def test_produces_castable_instances_from_name(name):
+@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+def test_produces_castable_instances_from_name(xp, xps, dtype_name):
     """Strategies inferred by dtype name generate values of a builtin type
     castable to the dtype."""
-    builtin = find_castable_builtin_for_dtype(xp, getattr(xp, name))
-
-    @given(xps.from_dtype(name))
-    def test_is_builtin(value):
-        assert isinstance(value, builtin)
-
-    test_is_builtin()
+    dtype = getattr(xp, dtype_name)
+    builtin = find_castable_builtin_for_dtype(xp, dtype)
+    assert_all_examples(xps.from_dtype(dtype_name), lambda v: isinstance(v, builtin))
 
 
-@pytest.mark.parametrize("dtype", DTYPES)
-def test_passing_inferred_strategies_in_arrays(dtype):
+@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+def test_passing_inferred_strategies_in_arrays(xp, xps, dtype_name):
     """Inferred strategies usable in arrays strategy."""
-    elements = xps.from_dtype(dtype)
-
-    @given(xps.arrays(dtype, 10, elements=elements))
-    def smoke_test(_):
-        pass
-
-    smoke_test()
+    elements = xps.from_dtype(dtype_name)
+    find_any(xps.arrays(dtype_name, 10, elements=elements))
 
 
 @pytest.mark.parametrize(
     "dtype, kwargs, predicate",
     [
         # Floating point: bounds, exclusive bounds, and excluding nonfinites
-        (xp.float32, {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2),
+        ("float32", {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2),
         (
-            xp.float32,
+            "float32",
             {"min_value": 1, "max_value": 2, "exclude_min": True, "exclude_max": True},
             lambda x: 1 < x < 2,
         ),
-        (xp.float32, {"allow_nan": False}, lambda x: not math.isnan(x)),
-        (xp.float32, {"allow_infinity": False}, lambda x: not math.isinf(x)),
-        (xp.float32, {"allow_nan": False, "allow_infinity": False}, math.isfinite),
+        ("float32", {"allow_nan": False}, lambda x: not math.isnan(x)),
+        ("float32", {"allow_infinity": False}, lambda x: not math.isinf(x)),
+        ("float32", {"allow_nan": False, "allow_infinity": False}, math.isfinite),
         # Integer bounds, limited to the representable range
-        (xp.int8, {"min_value": -1, "max_value": 1}, lambda x: -1 <= x <= 1),
-        (xp.uint8, {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2),
+        ("int8", {"min_value": -1, "max_value": 1}, lambda x: -1 <= x <= 1),
+        ("uint8", {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2),
     ],
 )
-@given(data=st.data())
-def test_from_dtype_with_kwargs(data, dtype, kwargs, predicate):
+def test_from_dtype_with_kwargs(xp, xps, dtype, kwargs, predicate):
     """Strategies inferred with kwargs generate values in bounds."""
     strat = xps.from_dtype(dtype, **kwargs)
-    value = data.draw(strat)
-    assert predicate(value)
+    assert_all_examples(strat, predicate)
 
 
-def test_can_minimize_floats():
+def test_can_minimize_floats(xp, xps):
     """Inferred float strategy minimizes to a good example."""
     smallest = minimal(xps.from_dtype(xp.float32), lambda n: n >= 1.0)
     assert smallest == 1
 
 
 smallest_normal = width_smallest_normals[32]
-subnormal_strats = [
-    xps.from_dtype(xp.float32),
-    xps.from_dtype(xp.float32, min_value=-1),
-    xps.from_dtype(xp.float32, max_value=1),
-    xps.from_dtype(xp.float32, max_value=1),
-    pytest.param(
-        xps.from_dtype(xp.float32, min_value=-1, max_value=1),
-        marks=pytest.mark.skip(
-            reason="FixedBoundFloatStrategy(0, 1) rarely generates subnormals"
-        ),
-    ),
-]
-
-
-@pytest.mark.skipif(
-    WIDTHS_FTZ[32], reason="Subnormals should not be generated for FTZ builds"
-)
-@pytest.mark.parametrize("strat", subnormal_strats)
-def test_generate_subnormals_for_non_ftz_float32(strat):
-    find_any(
-        strat.filter(lambda n: n != 0), lambda n: -smallest_normal < n < smallest_normal
-    )
 
 
-@pytest.mark.skipif(
-    not WIDTHS_FTZ[32], reason="Subnormals should be generated for non-FTZ builds"
+@pytest.mark.parametrize(
+    "kwargs",
+    [
+        {},
+        {"min_value": -1},
+        {"max_value": 1},
+        pytest.param(
+            {"min_value": -1, "max_value": 1},
+            marks=pytest.mark.skip(
+                reason="FixedBoundFloatStrategy(0, 1) rarely generates subnormals"
+            ),
+        ),
+    ],
 )
-@pytest.mark.parametrize("strat", subnormal_strats)
-def test_does_not_generate_subnormals_for_ftz_float32(strat):
-    assert_no_examples(
-        strat.filter(lambda n: n != 0), lambda n: -smallest_normal < n < smallest_normal
-    )
+def test_subnormal_generation(xp, xps, kwargs):
+    """Generation of subnormals is dependent on FTZ behaviour of array module."""
+    strat = xps.from_dtype(xp.float32, **kwargs).filter(lambda n: n != 0)
+    if flushes_to_zero(xp, width=32):
+        assert_no_examples(strat, lambda n: -smallest_normal < n < smallest_normal)
+    else:
+        find_any(strat, lambda n: -smallest_normal < n < smallest_normal)
diff --git a/hypothesis-python/tests/array_api/test_indices.py b/hypothesis-python/tests/array_api/test_indices.py
--- a/hypothesis-python/tests/array_api/test_indices.py
+++ b/hypothesis-python/tests/array_api/test_indices.py
@@ -10,13 +10,12 @@
 
 import math
 
-from hypothesis import assume, given, strategies as st
+from hypothesis import assume, given, note, strategies as st
 
-from tests.array_api.common import xp, xps
-from tests.common.debug import find_any
+from tests.common.debug import assert_all_examples, find_any
 
 
-def test_generate_indices_with_and_without_ellipsis():
+def test_generate_indices_with_and_without_ellipsis(xp, xps):
     """Strategy can generate indices with and without Ellipsis."""
     strat = (
         xps.array_shapes(min_dims=1, max_dims=32)
@@ -27,21 +26,23 @@ def test_generate_indices_with_and_without_ellipsis():
     find_any(strat, lambda ix: Ellipsis not in ix)
 
 
-@given(xps.indices(shape=(), allow_ellipsis=True))
-def test_generate_indices_for_0d_shape(idx):
+def test_generate_indices_for_0d_shape(xp, xps):
     """Strategy only generates empty tuples or Ellipsis as indices for an empty
     shape."""
-    assert idx in [(), Ellipsis, (Ellipsis,)]
+    assert_all_examples(
+        xps.indices(shape=(), allow_ellipsis=True),
+        lambda idx: idx in [(), Ellipsis, (Ellipsis,)],
+    )
 
 
-def test_generate_tuples_and_non_tuples_for_1d_shape():
+def test_generate_tuples_and_non_tuples_for_1d_shape(xp, xps):
     """Strategy can generate tuple and non-tuple indices with a 1-dimensional shape."""
     strat = xps.indices(shape=(1,), allow_ellipsis=True)
     find_any(strat, lambda ix: isinstance(ix, tuple))
     find_any(strat, lambda ix: not isinstance(ix, tuple))
 
 
-def test_generate_long_ellipsis():
+def test_generate_long_ellipsis(xp, xps):
     """Strategy can replace runs of slice(None) with Ellipsis.
 
     We specifically test if [0,...,0] is generated alongside [0,:,:,:,0]
@@ -55,31 +56,31 @@ def test_generate_long_ellipsis():
     )
 
 
-@given(
-    xps.indices(shape=(0, 0, 0, 0, 0), max_dims=5).filter(
-        lambda idx: isinstance(idx, tuple) and Ellipsis in idx
-    )
-)
-def test_indices_replaces_whole_axis_slices_with_ellipsis(idx):
+def test_indices_replaces_whole_axis_slices_with_ellipsis(xp, xps):
     # `slice(None)` (aka `:`) is the only valid index for an axis of size
     # zero, so if all dimensions are 0 then a `...` will replace all the
     # slices because we generate `...` for entire contiguous runs of `:`
-    assert slice(None) not in idx
+    assert_all_examples(
+        xps.indices(shape=(0, 0, 0, 0, 0), max_dims=5).filter(
+            lambda idx: isinstance(idx, tuple) and Ellipsis in idx
+        ),
+        lambda idx: slice(None) not in idx,
+    )
 
 
-@given(xps.indices((3, 3, 3, 3, 3)))
-def test_efficiently_generate_indexers(_):
+def test_efficiently_generate_indexers(xp, xps):
     """Generation is not too slow."""
+    find_any(xps.indices((3, 3, 3, 3, 3)))
 
 
-@given(
-    shape=xps.array_shapes(min_dims=1, max_side=4)
-    | xps.array_shapes(min_dims=1, min_side=0, max_side=10),
-    allow_ellipsis=st.booleans(),
-    data=st.data(),
-)
-def test_generate_valid_indices(shape, allow_ellipsis, data):
+@given(allow_ellipsis=st.booleans(), data=st.data())
+def test_generate_valid_indices(xp, xps, allow_ellipsis, data):
     """Strategy generates valid indices."""
+    shape = data.draw(
+        xps.array_shapes(min_dims=1, max_side=4)
+        | xps.array_shapes(min_dims=1, min_side=0, max_side=10),
+        label="shape",
+    )
     min_dims = data.draw(st.integers(0, len(shape)), label="min_dims")
     max_dims = data.draw(
         st.none() | st.integers(min_dims, len(shape)), label="max_dims"
@@ -120,4 +121,5 @@ def test_generate_valid_indices(shape, allow_ellipsis, data):
         # We can't cheat on this one, so just try another.
         assume(False)
     # Finally, check that we can use our indexer without error
+    note(f"{array=}")
     array[indexer]
diff --git a/hypothesis-python/tests/array_api/test_pretty.py b/hypothesis-python/tests/array_api/test_pretty.py
--- a/hypothesis-python/tests/array_api/test_pretty.py
+++ b/hypothesis-python/tests/array_api/test_pretty.py
@@ -12,8 +12,6 @@
 
 import pytest
 
-from tests.array_api.common import xp, xps
-
 
 @pytest.mark.parametrize(
     "name",
@@ -33,7 +31,7 @@
         "indices",
     ],
 )
-def test_namespaced_methods_meta(name):
+def test_namespaced_methods_meta(xp, xps, name):
     """Namespaced method objects have good meta attributes."""
     func = getattr(xps, name)
     assert func.__name__ == name
@@ -46,25 +44,34 @@ def test_namespaced_methods_meta(name):
 
 
 @pytest.mark.parametrize(
-    "name, strat",
+    "name, valid_args",
     [
-        ("from_dtype", xps.from_dtype(xp.int8)),
-        ("arrays", xps.arrays(xp.int8, 5)),
-        ("array_shapes", xps.array_shapes()),
-        ("scalar_dtypes", xps.scalar_dtypes()),
-        ("boolean_dtypes", xps.boolean_dtypes()),
-        ("numeric_dtypes", xps.numeric_dtypes()),
-        ("integer_dtypes", xps.integer_dtypes()),
-        ("unsigned_integer_dtypes", xps.unsigned_integer_dtypes()),
-        ("floating_dtypes", xps.floating_dtypes()),
-        ("valid_tuple_axes", xps.valid_tuple_axes(0)),
-        ("broadcastable_shapes", xps.broadcastable_shapes(())),
-        ("mutually_broadcastable_shapes", xps.mutually_broadcastable_shapes(3)),
-        ("indices", xps.indices((5,))),
+        ("from_dtype", ["int8"]),
+        ("arrays", ["int8", 5]),
+        ("array_shapes", []),
+        ("scalar_dtypes", []),
+        ("boolean_dtypes", []),
+        ("numeric_dtypes", []),
+        ("integer_dtypes", []),
+        ("unsigned_integer_dtypes", []),
+        ("floating_dtypes", []),
+        ("valid_tuple_axes", [0]),
+        ("broadcastable_shapes", [()]),
+        ("mutually_broadcastable_shapes", [3]),
+        ("indices", [(5,)]),
     ],
 )
-def test_namespaced_strategies_repr(name, strat):
+def test_namespaced_strategies_repr(xp, xps, name, valid_args):
     """Namespaced strategies have good repr."""
+    func = getattr(xps, name)
+    strat = func(*valid_args)
     assert repr(strat).startswith(name + "("), f"{name} not in strat repr {strat!r}"
     assert len(repr(strat)) < 100, "strat repr looks too long"
     assert xp.__name__ not in repr(strat), f"{xp.__name__} in strat repr"
+
+
+def test_strategies_namespace_repr(xp, xps):
+    """Strategies namespace has good repr."""
+    expected = f"make_strategies_namespace({xp.__name__})"
+    assert repr(xps) == expected
+    assert str(xps) == expected
diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py
--- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py
+++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py
@@ -10,54 +10,61 @@
 
 import pytest
 
-from hypothesis import given
-from hypothesis.extra.array_api import DTYPE_NAMES, INT_NAMES, NUMERIC_NAMES, UINT_NAMES
+from hypothesis.extra.array_api import (
+    DTYPE_NAMES,
+    FLOAT_NAMES,
+    INT_NAMES,
+    NUMERIC_NAMES,
+    UINT_NAMES,
+)
 
-from tests.array_api.common import xp, xps
-from tests.common.debug import minimal
+from tests.common.debug import assert_all_examples, find_any, minimal
 
 
-@given(xps.scalar_dtypes())
-def test_can_generate_scalar_dtypes(dtype):
-    assert dtype in (getattr(xp, name) for name in DTYPE_NAMES)
+def test_can_generate_scalar_dtypes(xp, xps):
+    dtypes = [getattr(xp, name) for name in DTYPE_NAMES]
+    assert_all_examples(xps.scalar_dtypes(), lambda dtype: dtype in dtypes)
 
 
-@given(xps.boolean_dtypes())
-def test_can_generate_boolean_dtypes(dtype):
-    assert dtype == xp.bool
+def test_can_generate_boolean_dtypes(xp, xps):
+    assert_all_examples(xps.boolean_dtypes(), lambda dtype: dtype == xp.bool)
 
 
-@given(xps.numeric_dtypes())
-def test_can_generate_numeric_dtypes(dtype):
-    assert dtype in (getattr(xp, name) for name in NUMERIC_NAMES)
+def test_can_generate_numeric_dtypes(xp, xps):
+    numeric_dtypes = [getattr(xp, name) for name in NUMERIC_NAMES]
+    assert_all_examples(xps.numeric_dtypes(), lambda dtype: dtype in numeric_dtypes)
 
 
-@given(xps.integer_dtypes())
-def test_can_generate_integer_dtypes(dtype):
-    assert dtype in (getattr(xp, name) for name in INT_NAMES)
+def test_can_generate_integer_dtypes(xp, xps):
+    int_dtypes = [getattr(xp, name) for name in INT_NAMES]
+    assert_all_examples(xps.integer_dtypes(), lambda dtype: dtype in int_dtypes)
 
 
-@given(xps.unsigned_integer_dtypes())
-def test_can_generate_unsigned_integer_dtypes(dtype):
-    assert dtype in (getattr(xp, name) for name in UINT_NAMES)
+def test_can_generate_unsigned_integer_dtypes(xp, xps):
+    uint_dtypes = [getattr(xp, name) for name in UINT_NAMES]
+    assert_all_examples(
+        xps.unsigned_integer_dtypes(), lambda dtype: dtype in uint_dtypes
+    )
 
 
-@given(xps.floating_dtypes())
-def test_can_generate_floating_dtypes(dtype):
-    assert dtype in (getattr(xp, name) for name in DTYPE_NAMES)
+def test_can_generate_floating_dtypes(xp, xps):
+    float_dtypes = [getattr(xp, name) for name in FLOAT_NAMES]
+    assert_all_examples(xps.floating_dtypes(), lambda dtype: dtype in float_dtypes)
 
 
-def test_minimise_scalar_dtypes():
+def test_minimise_scalar_dtypes(xp, xps):
     assert minimal(xps.scalar_dtypes()) == xp.bool
 
 
 @pytest.mark.parametrize(
-    "strat_func, sizes",
+    "strat_name, sizes",
     [
-        (xps.integer_dtypes, 8),
-        (xps.unsigned_integer_dtypes, 8),
-        (xps.floating_dtypes, 32),
+        ("integer_dtypes", 8),
+        ("unsigned_integer_dtypes", 8),
+        ("floating_dtypes", 32),
     ],
 )
-def test_can_specify_sizes_as_an_int(strat_func, sizes):
-    strat_func(sizes=sizes).example()
+def test_can_specify_sizes_as_an_int(xp, xps, strat_name, sizes):
+    strat_func = getattr(xps, strat_name)
+    strat = strat_func(sizes=sizes)
+    find_any(strat)