Написание собственного arbitrary
Gen покрывает типовые случаи, но любое пространство значений достижимо прямой реализацией ArbitraryInterface: generate(Random) возвращает Shrinkable — вытянутое значение плюс ленивое дерево меньших кандидатов, наиболее агрессивных первыми, каждый со своим поддеревом. Случайность тяните только через внедрённый Random (int(), float(), bytes()), чтобы прогоны с seed оставались воспроизводимыми.
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);
}
});
}
}Собственный arbitrary используется как любой встроенный: верните его из метода-генератора с ключом по имени параметра. Shrinkable::leaf($value) строит терминальный узел (без кандидатов); Shrinkable::of($value, $closure) навешивает лениво вычисляемых кандидатов; Shrinkable::map($fn) трансформирует всё дерево. Держите каждую ветвь дерева конечной и никогда не yield'айте кандидата, равного родителю, — именно это гарантирует завершение shrinking'а.