Skip to content

Boundary bias

Равномерная случайная выборка тратит большинство прогонов на неинтересную середину диапазона. Баги кластеризуются на краях — 0, ±1, точные границы — поэтому числовые генераторы намеренно смещены к ним, а не сэмплируют равномерно.

Как это работает

Gen::int(), Gen::intBetween(), Gen::float() и Gen::floatBetween() на каждом draw кидают шанс 1 из 5 (BIAS_DENOMINATOR = 5 у IntArbitrary/FloatArbitrary). При срабатывании вместо равномерного значения возвращается один из in-range boundary-кандидатов, вычисленных внутренним хелпером Boundary:

  • Целые: различные значения среди 0, 1, -1, $min, $max, попадающие в [$min, $max]. $min и $max всегда в диапазоне по построению, поэтому список кандидатов никогда не пуст — диапазон вроде intBetween(10, 20) всё равно смещается к своим собственным 10 и 20, даже если 0/1/-1 все вне диапазона.
  • Float: различные значения среди 0.0, $min, попадающие в [$min, $max). FloatArbitrary генерирует полуоткрытый диапазон, поэтому сам $max намеренно исключён и из списка границ, и из обычной генерации — он никогда не может быть выдан как значение. Вырожденный диапазон, где ни 0.0, ни $min не подходят (пустой список границ), просто откатывается к равномерной выборке для этого draw.

Остальные 4 draw'а из 5 сэмплируют равномерно по всему диапазону, так что генератор всё ещё исследует внутреннюю часть — bias смещает вероятности, а не заменяет покрытие.

Shrinking не затронут

Boundary bias меняет только то, что возвращает генерация; он ничего не говорит о том, как значение шринкается. Оба arbitrary шринкают к одной и той же цели независимо от того, как значение было получено — IntArbitrary делением пополам расстояния до max($min, min($max, 0)), FloatArbitrary единственным кандидатом max($min, min($max, 0.0)). Boundary-biased draw и равномерно сэмплированный draw с тем же значением дают идентичные shrink-деревья.

Почему это важно при проектировании property

  • Не пишите Gen::filter($ints, fn($n) => $n !== 0), рассчитывая, что фильтр сработает редко — с шансом boundary-bias 1 из 5 попасть в 0 (когда он в диапазоне), плюс 0 как частая цель обычного shrinking, такой фильтр отбрасывает значительно чаще, чем предполагала бы чисто равномерная модель. Предпочитайте Gen::intBetween(1, $max) (сконструировать домен) вместо фильтрации нуля из него.
  • Property, которое «обычно проходит, но иногда падает на границах» — ровно то, для быстрого выявления чего создан этот bias: не нужен runs: 10000, чтобы попасть в min/max/0; уже несколько сотен прогонов сэмплируют их многократно.
  • Gen::floatSpecial() — отдельный opt-in механизм для NAN/±INF/-0.0; дефолтный bias float()/floatBetween() намеренно остаётся полностью в диапазоне и конечным. Полный каталог — на странице обзора генераторов.