Generating and Shrinking Samples¶
When a property-based test fails, the array it reports is usually small and
simple — a short list, a single small number — rather than a large, complex one.
That is by design. The Testing Awkward Array page
calls the reported array a "minimal failing array"; this page explains where it
comes from: how hypothesis-awkward builds a random Awkward Array, and how it
reduces a failing one.
This is a deep dive into how the strategies work, written for Awkward Array contributors and for authors of Hypothesis strategies. If you only want to generate test arrays for your own code, start with Getting Started. Property-based testing (PBT) runs one test against many generated inputs; when one input fails, Hypothesis reduces it to a smaller input that still fails — it shrinks the example.
Generating a sample¶
The main strategy, st_ak.constructors.arrays(), generates a nearly fully
general Awkward Array. Underneath, st_ak.contents.contents() builds the
array's layout from the top down: at each step it either stops at a leaf or
chooses a wrapper layout and then builds that wrapper's child with the same
strategy.
- Leaves carry the data: an
EmptyArray(no data, typeunknown), aNumpyArrayof one dtype, or a string or bytestring. (A string or bytestring is itself aListOffsetArraywrapping aNumpyArrayofuint8, which the strategy treats as a single leaf.) - Wrappers add structure around a child:
RegularArray,ListOffsetArray,ListArray,RecordArray,UnionArray, and the option (missing-value) types.
Because a wrapper's child is generated by the same strategy, the layout grows
recursively into a tree. Generation is bounded so that it always terminates:
max_size is a budget over the scalar values in the layout (data elements,
offsets, indices, field names, and parameters), and max_depth,
max_leaf_size, min_length, and max_length constrain it further. When a
layout has several children, the element budget is shared across them so that
the total stays within max_size.
These bounds are also knobs: passing a dtypes strategy, setting the allow_*
flags, or fixing min_length and max_length lets a caller constrain the shape
of what is generated. See the
Contents reference for every option.
from hypothesis import given
import awkward as ak
import hypothesis_awkward.strategies as st_ak
@given(a=st_ak.constructors.arrays())
def test_some_property(a: ak.Array) -> None: ...
The dtype of a NumpyArray leaf is itself drawn from a strategy,
st_ak.supported_dtypes().
How Hypothesis shrinks¶
When a test fails, Hypothesis searches for a smaller input that still fails. The Hypothesis guide Strategies that shrink describes the model: the engine "sees every example as a labelled tree of choices," and it "shrinks from the 'bottom up'. If any component of your strategy is replaced with a simpler example, the end result should also become simpler."
There is therefore no single setting that makes shrinking good. The quality emerges from how each choice in a strategy is arranged: if every individual choice shrinks toward something simple, the array built from those choices does too.
Shrinking toward simpler data¶
hypothesis-awkward cannot change how Hypothesis shrinks; it can only change
the order in which it offers each choice. Hypothesis's one_of() and
sampled_from() both shrink toward their earlier entries, so each strategy
lists its alternatives simplest first. This tree of choices is the same tree the
strategy built while generating, so a sample shrinks well exactly when each of
its choices does.
"Simpler" here means simpler data — what the array's values and shape look
like — rather than the internal layout used to store them. For example, a list
layout can hold unreachable data: content elements that no sublist refers to,
left by offsets that do not start at 0 or do not reach the end of the content.
Two layouts can describe the same list-of-lists while one carries unreachable
data and the other does not. The strategies treat the layout without unreachable
data as simpler and shrink toward it.
A contents() node also shrinks toward a leaf rather than a wrapper, and among
wrappers toward RegularArray (the first wrapper offered).
What each strategy shrinks toward¶
Each strategy below lives in st_ak.contents, except supported_dtypes(),
which is st_ak.supported_dtypes().
| Strategy | Shrinks toward |
|---|---|
leaf_contents() |
EmptyArray, then bytestring, string, and NumpyArray |
supported_dtypes() |
bool, then the rest of the dtypes that have Python built-in counterparts (int64, float64, complex128, datetime64[us], timedelta64[us]), then the remaining dtypes |
masked_contents() |
UnmaskedArray, then ByteMaskedArray, then BitMaskedArray |
regular_array_contents() |
fewer groups (a shorter outer length) with a larger size, using divisors of the content length so that no data is unreachable |
list_offset_array_contents() † |
no unreachable data: offsets[0] == 0 and offsets[-1] == len(content) |
list_array_contents() † |
no unreachable data, with contiguous, monotonic, non-overlapping starts/stops |
† Best-effort and not reliably reached; see Limitations.
Verifying shrink quality¶
Each shrink direction in the table has a test; the simplest is the dtype case.
The tests call hypothesis.find() with the example database disabled, then
assert the minimal value:
>>> from hypothesis import find, settings
>>> import hypothesis_awkward.strategies as st_ak
>>> find(st_ak.supported_dtypes(), lambda d: True, settings=settings(database=None))
dtype('bool')
find() searches for the simplest value it can find that satisfies the
predicate — best-effort, like all shrinking. Here the predicate lambda d: True
accepts every dtype, so the result is the simplest dtype, bool. Setting
database=None stops Hypothesis from replaying a previously stored example, so
the result comes only from this run and the doctest stays reproducible. The full
set of shrink tests is under
tests/strategies/.
Limitations¶
Shrinking is best-effort: it searches for a minimal failing example but does not
guarantee the globally simplest one. The reachability of list layouts shows the
limit precisely. Generating the offsets of a ListOffsetArray in isolation
reliably shrinks to no unreachable data. In the composed
list_offset_array_contents() and list_array_contents() strategies, however —
where the child content is generated and shrunk at the same time — the shrinker
does not reliably reach that layout, so those two tests are marked xfail with
the reason "shrinker does not reliably reach no-unreachable layout."
For the general theory, and advice on writing strategies that shrink well, see the Hypothesis guide Strategies that shrink.