Writing your own arbitrary
Gen covers common cases, but any value space is reachable by implementing ArbitraryInterface directly: generate(Random) returns a Shrinkable — the drawn value plus a lazy tree of smaller candidates, most aggressive first, each carrying its own subtree. Draw randomness only through the injected Random (int(), float(), bytes()) so seeded runs stay reproducible.
use Rasuvaeff\PropertyTesting\ArbitraryInterface;
use Rasuvaeff\PropertyTesting\Random;
use Rasuvaeff\PropertyTesting\Shrinkable;
/**
* Even integers in [0, $max], shrinking toward 0 in even steps.
*/
final readonly class EvenArbitrary implements ArbitraryInterface
{
public function __construct(private int $max = 1000) {}
#[\Override]
public function generate(Random $random): Shrinkable
{
return $this->tree($random->int(0, intdiv($this->max, 2)) * 2);
}
private function tree(int $value): Shrinkable
{
return Shrinkable::of($value, function () use ($value): \Generator {
if ($value === 0) {
return;
}
yield $this->tree(0);
$half = intdiv($value, 4) * 2; // stay even
if ($half !== 0 && $half !== $value) {
yield $this->tree($half);
}
});
}
}A custom arbitrary is used like any built-in: return it from the generators method keyed by parameter name. Shrinkable::leaf($value) builds a terminal node (no candidates); Shrinkable::of($value, $closure) attaches lazily computed candidates; Shrinkable::map($fn) transforms a whole tree. Keep every branch of the tree finite and never yield a candidate equal to its parent — that is what guarantees shrinking terminates.