Skip to content

Зависимые генераторы: flatMap vs Gen::draw()

Зависимые генераторы (flatMap)

Когда домен одного входа зависит от другого — список плюс валидный индекс в нём, размер плюс payload этого размера — Gen::flatMap() передаёт каждое сгенерированное значение в замыкание, возвращающее arbitrary для финального значения. В отличие от Assume::that()-гарда, прогоны не отбрасываются, а shrink'аются оба уровня: сначала исходное значение (зависимое регенерируется детерминированно из seed прогона), затем зависимое при фиксированном исходном.

php
/** @return array<string, ArbitraryInterface> */
public static function sliceGenerators(): array
{
    return ['pair' => Gen::flatMap(
        Gen::nonEmptyArrayOf(Gen::int()),
        static fn(array $items): ArbitraryInterface => Gen::tuple(
            Gen::constant($items),
            Gen::intBetween(0, count($items) - 1), // always a valid index
        ),
    )];
}

Draw внутри тела (Gen::draw)

Если несколько зависимых значений делают вложенный flatMap громоздким, draw'те их внутри тела свойства через Gen::draw(). Домен может зависеть от чего угодно уже в скоупе — параметров, предыдущих draw'ов, промежуточных результатов:

php
#[Property(runs: 200)]
public function sliceIsContainedInTheList(array $xs): void
{
    $from = Gen::draw(Gen::intBetween(0, count($xs)));
    $to = Gen::draw(Gen::intBetween($from, count($xs))); // depends on $from

    foreach (array_slice($xs, $from, $to - $from) as $item) {
        Assert::true(in_array($item, $xs, true));
    }
}

Draw-значения shrink'аются вместе с параметрами. Раннер записывает каждый draw на ленту повторов; при падении свойства shrink'ает каждый записанный draw через его собственное дерево и перезапускает тело с лентой, проигранной по позиции. Shrink'нутый параметр может изменить control flow тела: draw'ы за концом ленты генерируются заново, а draw'ы, до которых укороченный прогон больше не доходит, отбрасываются. Контрпримеры сообщают draw'и как draw#1, draw#2, ... рядом с именованными параметрами (а PROPERTY_VERBOSE логирует их на каждый прогон).

Два важных момента:

  • Проигранный draw отдаётся по позиции и не перевалидируется против (возможно, более узкого) arbitrary'я нового control flow — та же модель, что у gen() в fast-check. Утверждайте то, что телу реально нужно, а не полагайтесь на диапазон draw'а после shrinking'а.
  • Поскольку лента может отрасти заново во время shrinking'а, аргумент конечности дерева сам по себе больше не работает; при наличии draw'ов принимаемые shrink-шаги ограничены (1000 по умолчанию, maxShrinks при установке всё равно побеждает).

Gen::draw() валиден только пока раннер выполняет тело свойства; в любом другом месте бросает исключение. Для одиночного зависимого значения предпочитайте flatMap — он держит весь домен видимым в методе-генераторе.