Зависимые генераторы: flatMap vs Gen::draw()
Зависимые генераторы (flatMap)
Когда домен одного входа зависит от другого — список плюс валидный индекс в нём, размер плюс payload этого размера — Gen::flatMap() передаёт каждое сгенерированное значение в замыкание, возвращающее arbitrary для финального значения. В отличие от Assume::that()-гарда, прогоны не отбрасываются, а shrink'аются оба уровня: сначала исходное значение (зависимое регенерируется детерминированно из seed прогона), затем зависимое при фиксированном исходном.
/** @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'ов, промежуточных результатов:
#[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 — он держит весь домен видимым в методе-генераторе.