Skip to content

Написание собственного arbitrary

Gen покрывает типовые случаи, но любое пространство значений достижимо прямой реализацией ArbitraryInterface: generate(Random) возвращает Shrinkable — вытянутое значение плюс ленивое дерево меньших кандидатов, наиболее агрессивных первыми, каждый со своим поддеревом. Случайность тяните только через внедрённый Random (int(), float(), bytes()), чтобы прогоны с seed оставались воспроизводимыми.

php
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'а.