Integrated shrinking
generate() returns value + lazy shrink tree together — transformed generators (map, flatMap) shrink correctly for free.
Property-based testing for PHP 8.3+, built as a plugin for Testo.
Property falsified after 246 successful run(s); seed=7382910
Original: maxAttempts=17, baseSeconds=91, cap=847, attempts=23
Shrunk: maxAttempts=1, baseSeconds=848, cap=847, attempts=1 (12 shrink step(s), 41 trial(s))
Changed: maxAttempts=17 -> 1, baseSeconds=91 -> 848, attempts=23 -> 1Four generated arguments went in; the Changed: line tells you only three of them actually drive the failure — the shrinker found that by searching, you didn't have to step through a debugger to see it.
// The three pieces in isolation, no Testo runner involved.
$ints = Gen::intBetween(0, 1000);
$failing = null;
for ($run = 0; $run < 100; ++$run) {
$shrinkable = $ints->generate($random);
if ($shrinkable->value % 2 !== 0) {
$failing = $shrinkable;
break;
}
}
// -> shrink toward the simplest odd int in range#[Test]
final class ListReversalProperties
{
#[Property(runs: 200)]
public function reversingTwiceRestoresTheList(array $xs): void
{
Assert::same(array_reverse(array_reverse($xs)), $xs);
}
/** @return array<string, ArbitraryInterface> */
public static function reversingTwiceRestoresTheListGenerators(): array
{
return ['xs' => Gen::arrayOf(Gen::intBetween(-100, 100))];
}
}// Sample a generator directly — a quick way to eyeball what it produces.
Gen::sample(Gen::intBetween(1, 6), count: 5, seed: 42);
// [3, 1, 6, 6, 2]
Gen::sampleShrinks(Gen::intBetween(0, 100), seed: 1);
// ['value' => 87, 'shrinks' => [0, 44, 66, 77, 82, 85, 86]]#[Property(runs: 200)]
public function stackBehavesLikeItsModel(CommandSequence $sequence): void
{
StateMachine::check($sequence, static fn(): ExampleStack => new ExampleStack());
}
/** @return array<string, ArbitraryInterface> */
public static function stackBehavesLikeItsModelGenerators(): array
{
return ['sequence' => Gen::commands([], [
Gen::map(Gen::intBetween(0, 99), static fn(int $v) => new Push($v)),
Gen::constant(new Pop()),
])];
}Full, runnable versions of all four live in examples/ — see the Examples page for what each one shows.