Skip to content

Gen ​

Rasuvaeff\PropertyTesting\Gen

Class — Package: property-testing-core — Source — Version: v1.0.0-1-g88fdee2

Facade with static factories for the built-in ArbitraryInterfaces.

Each factory returns a ready-to-use arbitrary; values are never generated directly through Gen — that happens inside the property runner, which threads the seedable Random through every generator so runs are reproducible. The one exception is sample(), a debugging aid that eagerly generates values from a given arbitrary.

Methods ​

int() ​

php
static int(): ArbitraryInterface

Integers spanning PHP_INT_MIN..PHP_INT_MAX.

intBetween() ​

php
static intBetween(int<min, max> $min, int<min, max> $max): ArbitraryInterface

intPositive() ​

php
static intPositive(): ArbitraryInterface

Positive integers (1..PHP_INT_MAX).

float() ​

php
static float(): ArbitraryInterface

Floats in the half-open range [0.0, 1.0).

floatBetween() ​

php
static floatBetween(float $min, float $max): ArbitraryInterface

Floats in the half-open range [$min, $max) — $max itself is never drawn, exactly as float() never draws 1.0. A degenerate range ($min === $max) has that one value. Shrinks to the point of the range nearest to zero, which is never $max: for a range at or below zero that is the largest float under it.

bool() ​

php
static bool(): ArbitraryInterface

string() ​

php
static string(): ArbitraryInterface

Unicode strings of length 0..100.

stringAscii() ​

php
static stringAscii(): ArbitraryInterface

Printable ASCII strings of length 0..100.

stringOf() ​

php
static stringOf(
    int<0, max> $minLength = 0,
    int<1, max> $maxLength = 100,
): ArbitraryInterface

Unicode strings of a bounded length: string() with the bounds chosen, and the same defaults as stringFrom() and bytes().

char() ​

php
static char(): ArbitraryInterface

A single printable ASCII character.

stringFrom() ​

php
static stringFrom(
    string $alphabet,
    int $minLength = 0,
    int $maxLength = 100,
): ArbitraryInterface

Strings whose characters come from a fixed alphabet (split per Unicode codepoint). Shrinks by length toward '', then each character toward the first alphabet character — list simpler characters first.

bytes() ​

php
static bytes(int $minLength = 0, int $maxLength = 100): ArbitraryInterface

Raw byte strings (every byte 0..255). Shrinks by length toward '', then each byte toward "\x00".

arrayOf() ​

php
static arrayOf(
    \ArbitraryInterface<\TElement> $element,
    int $minSize = 0,
    int $maxSize = 100,
): ArbitraryInterface

Lists whose elements are drawn from $element.

nonEmptyArrayOf() ​

php
static nonEmptyArrayOf(
    \ArbitraryInterface<\TElement> $element,
    int $maxSize = 100,
): ArbitraryInterface

Non-empty lists whose elements are drawn from $element.

uniqueArrayOf() ​

php
static uniqueArrayOf(
    \ArbitraryInterface<\TElement> $element,
    int $minSize = 0,
    int $maxSize = 100,
    null|callable $by = NULL,
): ArbitraryInterface

Lists of pairwise-distinct elements drawn from $element. Element shrinking keeps the list distinct; the result may be smaller than the drawn size when the element space runs out of fresh values, but never below $minSize — an unreachable minimum throws GenerationExhaustedException.

  • $by — The key each element is distinct by; null compares the values.

Without $by, distinct means !== on the values: NAN is never identical to itself, so a list over floatSpecial() can hold several of them, and objects are distinct unless they are the same instance. With $by, distinct means === on the int|string key the closure returns for each value — uniqueness by one field of a value object, with shrinking that still never produces two elements sharing a key:

Gen::uniqueArrayOf($userGen, 3, 10, by: static fn (User $u): string => $u->id)

A key of any other type is refused with \InvalidArgumentException at generation time; identity comparison of key objects would make every element unique and void the guarantee without a failure.

withEdgeCases() ​

php
static withEdgeCases(\ArbitraryInterface<\T> $inner, \T $edgeCases): ArbitraryInterface

$inner with author-supplied boundary values: one draw in five is one of $edgeCases instead of a generated value, and a generated value shrinks through the edge values first — in the order listed, so put the most-preferred minimum first — before its own tree:

  • $inner — The generator to bias.
  • $edgeCases — The boundary values, most-preferred minimum first; at least one.

Gen::withEdgeCases(Gen::intBetween(0, $n), 0, $n, $n - 1) Gen::withEdgeCases(Gen::stringOf(), '', 'a')

The bias is explicit and scoped to this generator, so it stays on under Runner\EdgeCases::None, which turns off only the built-in boundary bias. The wrapper rolls on the run's randomness and leaves $inner's own sequence for a seed untouched. Edge values are taken as members of $inner's domain — nothing checks that they are.

subset() ​

php
static subset(
    list<\TValue> $values,
    int $minSize = 0,
    ?int $maxSize = NULL,
): ArbitraryInterface

Subsets of a fixed ordered set: every result is a list of distinct elements of $values, in the source order. Duplicates in $values are rejected with an InvalidArgumentException — a set has no duplicate members. The size is drawn uniformly from [minSize, maxSize] (null $maxSize means the full source size), then the combination of that size uniformly — small and large subsets appear equally often. Shrinking reduces the size first, then moves the kept elements toward earlier source positions; no filtering, no discards.

dictOf() ​

php
static dictOf(
    \ArbitraryInterface<\TKey> $key,
    \ArbitraryInterface<\TValue> $value,
    int $minSize = 0,
    int $maxSize = 100,
): ArbitraryInterface

Associative arrays (maps) with keys from $key and values from $value.

Keys must be int or string; only distinct keys are kept, so the result may be smaller than the drawn size when the key space runs out, but never below $minSize — an unreachable minimum throws GenerationExhaustedException. A string key PHP would store as an integer ("0", "12", "-3") is redrawn like a collision, so the map stays the array<string, T> a string key generator declares — a key generator producing only such strings yields [], or throws when $minSize is above zero.

record() ​

php
static record(array<string,\ArbitraryInterface> $shape): ArbitraryInterface

Fixed-shape associative array: each field is generated from its own arbitrary, keyed by field name. The property receives a single string-keyed array; shrinking reduces each field through its arbitrary while keeping the key set fixed.

  • $shape — Field name => arbitrary.

oneOf() ​

php
static oneOf(\TValue $values): ArbitraryInterface

Picks one of the given values at random.

Throws:

  • InvalidArgumentException — When no value is given, or when one of them is an \Rasuvaeff\PropertyTesting\ArbitraryInterface.

Values, not generators: Gen::oneOf(Gen::int(), Gen::string()) is rejected, because it would make the generator objects themselves the data. Use frequency() to pick between generators.

forClass() ​

php
static forClass(
    class-string<\TValue> $class,
    array<string,\ArbitraryInterface> $overrides = [],
    bool $skipInvalid = false,
    int $maxDepth = 3,
): ArbitraryInterface

Instances of $class, generated from what its constructor already declares — the reflection answer to jqwik's type-driven @ForAll and Rust's derive(Arbitrary), in a language that has no macros but does have promoted constructor properties and psalm annotations.

  • $class — The class to instantiate.
  • $overrides — Generators by constructor parameter name.
  • $skipInvalid — Whether a constructor that rejects a generated value discards it and redraws (as filter() does) instead of failing the run.
  • $maxDepth — How deep to follow class-typed parameters before refusing.
php
Gen::forClass(Money::class);
Gen::forClass(Money::class, ['amount' => Gen::intPositive()]);

Per parameter, in order: an override, then the docblock (psalm subset; @psalm-param/@phpstan-param over @param, and the promoted property's own @var when the constructor docblock is silent), then the native type. The docblock wins over the native type because it says more — int and int<0, 100> are the same native type and a very different value space — and a type this cannot read is an exception naming the parameter rather than a widened guess. See Arbitrary\ClassArbitrary for the supported subset and for what a validating constructor does.

forParameters() ​

php
static forParameters(
    \ReflectionFunctionAbstract $function,
    array<string,\ArbitraryInterface> $overrides = [],
    int $maxDepth = 3,
): array

Generators for a function's parameters, from what the signature already declares — forClass() applied to any function, method or closure instead of a constructor. This is the engine half of an adapter's auto mode: a property method whose parameters are fully typed needs no provider at all.

  • $function — The function, method or closure whose parameters to read.
  • $overrides — Generators by parameter name, winning over anything the parameter declares.
  • $maxDepth — How deep to follow class-typed parameters before refusing.
php
Gen::forParameters(new \ReflectionMethod(BackoffTest::class, 'delayStaysWithinCap'));
Gen::forParameters($method, ['cap' => Gen::intBetween(0, 60_000)]);   // override one parameter

Per parameter, in order: an override, then the docblock (psalm subset; @psalm-param/@phpstan-param over @param), then the native type — the same rules, the same supported subset and the same refusals as forClass(). Overrides may be partial: the parameters they name are taken as given, the rest are derived from the signature. A type this cannot read is an exception naming the function and the parameter, never a widened guess.

No skipInvalid here: there is no constructor to reject a value — this returns generators without executing anything, and a property body filters untrusted input through Assume, as always.

swarm() ​

php
static swarm(\ArbitraryInterface<\TValue> $arbitrary): ArbitraryInterface

Swarm testing over a choice generator: each generated case may only use some of $arbitrary's variants, drawn afresh per case and never empty.

  • $arbitrary — A choice generator: oneOf(), elements(), frequency(), commands(), or any Swarmable.
php
Gen::swarm(Gen::oneOf('push', 'pop', 'flush'));   // one case sees, say, only 'pop' and 'flush'
Gen::swarm(Gen::commands($model, $commands));   // one sequence uses a subset of the commands

Uniform draws from the full alphabet make every case look alike, and the bugs that need an operation to be absent stay out of reach. Shrinking stays inside the subset the case came from, so such a finding keeps reproducing — see Arbitrary\SwarmArbitrary for that and for the two consequences (scope of the draw, and what the counterexample reports).

elements() ​

php
static elements(array<array-key,\TValue> $values): ArbitraryInterface

Picks one value at random from an array (the array form of oneOf()).

  • $values — Must be non-empty.

Throws:

  • InvalidArgumentException — When the array is empty, or when one of its entries is an \Rasuvaeff\PropertyTesting\ArbitraryInterface.

Values, not generators — see oneOf().

constant() ​

php
static constant(\TValue $value): ArbitraryInterface

Always produces $value; does not shrink.

enum() ​

php
static enum(class-string<\TEnum> $enum): ArbitraryInterface

One case of a PHP enum, in declaration order. Shrinks toward earlier-declared cases, so declare simpler cases first.

floatSpecial() ​

php
static floatSpecial(): ArbitraryInterface

Special float values (NaN, ±INF, -0.0 and the representation edges) where float bugs cluster — an opt-in complement to float(), which stays inside its finite range. Shrinks toward earlier-listed specials.

intRange() ​

php
static intRange(int $min, int $max): ArbitraryInterface

Ordered integer pairs [lo, hi] with $min <= lo <= hi <= $max — the "range/interval" input without an Assume::that() discard. Built on flatMap(), so both bounds shrink while lo <= hi always holds.

recursive() ​

php
static recursive(
    ArbitraryInterface $leaf,
    callable $wrap,
    int $maxDepth = 3,
): ArbitraryInterface

Recursive structures with a bounded depth: $wrap receives the arbitrary for the previous level and returns the next one (e.g. wrap a value in an array). At every level generation picks the leaf or the wrapped branch with equal odds, so nesting is possible but not forced. Keep the branch fan-out small (bounded array sizes) — breadth multiplies per level.

Every level shrinks to its leaf first, so a nested value minimises to the plain value it wraps, not merely to an empty container.

nullable() ​

php
static nullable(ArbitraryInterface $inner): ArbitraryInterface

Yields null or a value from $inner with roughly even odds.

map() ​

php
static map(\ArbitraryInterface<\TInner> $inner, callable $map): ArbitraryInterface

Transforms each value produced by $inner through a pure function.

Shrinking happens in the source domain and the function is re-applied, so mapped values shrink through the inner arbitrary's tree.

flatMap() ​

php
static flatMap(
    \ArbitraryInterface<\TInner> $inner,
    callable $flatMap,
): ArbitraryInterface

Dependent generators (aka bind): feeds each value produced by $inner into $flatMap, which returns the arbitrary generating the final value.

Use it when one input's domain depends on another (e.g. an array plus a valid index into it) instead of discarding invalid combinations with Assume::that().

filter() ​

php
static filter(
    \ArbitraryInterface<\TInner> $inner,
    callable $predicate,
): ArbitraryInterface

Generates values from $inner, retrying until $predicate holds.

draw() ​

php
static draw(\ArbitraryInterface<\TValue> $arbitrary): mixed

In-body dependent draw: generates one value from $arbitrary inside the property body — for when several dependent values make nested flatMap() awkward. The domain may depend on anything already in scope, including previously drawn values:

php
#[Property(runs: 200)]
public function sliceIsContained(array $xs): void
{
    $from = Gen::draw(Gen::intBetween(0, count($xs)));
    $to = Gen::draw(Gen::intBetween($from, count($xs)));
    // ... assertions on array_slice($xs, $from, $to - $from) ...
}

Drawn values shrink together with the parameters: the runner records every draw on a replay tape, shrinks each recorded draw through its own tree, and re-runs the body with the tape replayed by position. A run that draws past the tape's end (control flow changed under a smaller prefix) generates the extra values anew. Counterexamples report draws as draw#1, draw#2, ... alongside the named parameters.

Only valid while the property runner executes the body; anywhere else it throws.

composite() ​

php
static composite(callable $body): ArbitraryInterface

A generator whose body draws several dependent values through a Draw and returns what it built — a reusable ArbitraryInterface where nested flatMap() calls would nest further right with every dependency:

  • $body — Builds one value from the draws it takes through the seam.

$interval = Gen::composite(static fn (Draw $d): Interval => new Interval( $min = $d->draw(Gen::datetime()), $d->draw(Gen::datetime(min: $min)), // sees the prior draw ));

Shrinking works on the draws, earliest first: a candidate re-executes the body with one draw replaced by a smaller one and the rest replayed, so a shrunk interval is still an interval the body would have built. A body that throws an Exception for a smaller draw refuses that candidate (skipped with its subtree, like map()). The body sees no other randomness — it must draw everything it needs — and the result composes like any generator: with map(), arrayOf(), a provider. Contrast in-body draw(), which exists only inside a property body and cannot be reused as a value.

The descent through a composite's draws is bounded at the same depth the runner applies to in-body draws (1000 accepted steps).

note() ​

php
static note(string $label, mixed $value): void

Attach a computed value to the counterexample report: the parsed form of a string, the delay a backoff chose, the index a search landed on — whatever the body derived and the assertion message does not carry.

  • $label — The note's name in the report.
  • $value — What to show beside it, rendered like an argument.

$encoded = encode($s); Gen::note('encoded', $encoded); Assert::same(decode($encoded), $s);

Notes belong to one run and surface only when that run is reported: the counterexample carries the notes of the original failing run and of the shrunk one, rendered after the arguments. A passing run's notes are dropped, so the cost is one array per run. Not a replacement for Classify::label(), which aggregates over the whole run set.

A later note under the same label replaces the earlier one. Outside a property run this throws, like draw().

tuple() ​

php
static tuple(ArbitraryInterface $elements): ArbitraryInterface

Fixed-arity tuple: one value per element arbitrary, in order. The property receives the tuple as a single array argument; shrinking reduces each position through its own arbitrary while keeping the arity fixed.

frequency() ​

php
static frequency(
    iterable<array{int, \ArbitraryInterface<\TValue>}> $pairs,
): ArbitraryInterface

Weighted choice among [weight, arbitrary] pairs: a branch is picked with probability proportional to its weight, then produces the value. Shrinking stays within the branch that generated the value.

  • $pairs — Weights must be >= 1.

uuid() ​

php
static uuid(): ArbitraryInterface

Canonical RFC 4122 version 4 UUID strings. Does not shrink.

randomEngine() ​

php
static randomEngine(): ArbitraryInterface

A Random\Engine whose randomness comes from the property's own draw tape, for code under test that takes a Random\Randomizer (or an engine): jittered backoff, shuffles, weighted picks, replica selection.

A fixed seed reproduces such a failure but cannot shrink it; this engine records each generate() as an in-body draw() of eight bytes, so the failing sequence of random decisions is replayed by position and shrunk through the bytes' own tree:

#[Property] public function backoffStaysUnderCap(int $attempt, Random\Engine $engine): void { $delay = (new JitteredBackoff(new Random\Randomizer($engine)))->delayMs($attempt);

Assert::true($delay <= 60_000); }

The bytes shrink toward "\0", which Randomizer::getInt($min, $max) maps to $min and shuffleArray() to a near-identity permutation — the natural minimum for most code. Each engine call is one tape position (draw#N in the counterexample) and the descent is bounded like every in-body draw, so a body that consumes thousands of random values per run shrinks only as far as that cap allows. Valid only inside a run. forParameters() derives it for a Random\Engine parameter, and a Random\Randomizer parameter wraps it.

randomizer() ​

php
static randomizer(): ArbitraryInterface

A Random\Randomizer over randomEngine(): the native API the code under test already accepts, with shrinkable randomness behind it.

datetime() ​

php
static datetime(
    ?DateTimeImmutable $min = NULL,
    ?DateTimeImmutable $max = NULL,
): ArbitraryInterface

UTC DateTimeImmutable values with a timestamp in the inclusive range [$min, $max] (defaults: 1970-01-01 .. 2100-01-01). Shrinks toward the Unix epoch, clamped to the range.

ipv4() ​

php
static ipv4(): ArbitraryInterface

IPv4 dotted-quad address strings ("0.0.0.0".."255.255.255.255"). Each octet shrinks toward 0 through its own integer tree.

ipv6() ​

php
static ipv6(): ArbitraryInterface

IPv6 address strings in the canonical text form of RFC 5952: lowercase hex, leading zeros stripped, and the longest run of zero groups compressed to :: (leftmost on a tie, never a single group).

Each of the eight 16-bit groups shrinks toward 0 through its own integer tree, so the descent walks through the shortened forms parsers get wrong — 2001:db8::1, fe80::, ::1 — and terminates at ::.

IPv4-mapped addresses (::ffff:1.2.3.4), zone ids (%eth0) and the bracketed URL form ([::1]:8080) are out of scope; Gen::url() emits no IPv6 host either.

email() ​

php
static email(): ArbitraryInterface

Syntactically valid local@label.tld email addresses over a lowercase alphanumeric alphabet and a small TLD set. Shrinks toward the shortest local part / label and the first TLD.

url() ​

php
static url(): ArbitraryInterface

HTTP/HTTPS URLs scheme://host.tld[/segment...] over a lowercase alphanumeric alphabet. Shrinks toward http://a.com (no path).

json() ​

php
static json(int $maxDepth = 3): ArbitraryInterface

A JSON-encodable value — null, bool, int, float, string, or nested lists/objects thereof — bounded to $maxDepth levels of nesting. Produces the decoded PHP value; use jsonString() for the encoded text.

jsonString() ​

php
static jsonString(int $maxDepth = 3): ArbitraryInterface

The JSON text of json() (json_encode of each generated value), for exercising JSON parsers and decoders.

regex() ​

php
static regex(string $pattern, int $maxRepeat = 8): ArbitraryInterface

Strings matching a regular-expression subset. The pattern is compiled to ordinary combinators, so matches shrink toward shorter/simpler strings.

  • $maxRepeat — Upper bound generation uses for unbounded quantifiers (*, +, {n,}).

Write the pattern without delimiters: [a-z]+, not /[a-z]+/. A delimited pattern is refused rather than compiled — the delimiters are ordinary characters to this compiler, so it would have generated strings beginning and ending with / and matching nothing the caller meant. Escape the character (\/) to match it literally.

Supported: literals, ., character classes [...] (ranges, negation, d\w\s and their negations, [\b] as a backspace), the escapes d\w\s\D\W\S\t\n\r plus \-escaped punctuation (the literal character), quantifiers * + ? {n} {n,} {n,m}, alternation |, and groups (...) / (?:...). A single leading ^ and trailing $ are accepted as no-ops. . and a negated class draw from printable ASCII (0x20..0x7E, so never a newline) — a subset of what the pattern matches, chosen so a generated string stays readable in a counterexample. Anchors elsewhere, backreferences, lookaround, named/inline groups, flags, lazy/possessive quantifiers, and any other alphanumeric escape (\h, \Q…\E, \0, ...) throw an \InvalidArgumentException naming the construct — compiled as literals they would generate non-matching strings.

stringMatching() ​

php
static stringMatching(string $pattern, int $maxRepeat = 8): ArbitraryInterface

Alias of regex() for parity with fast-check/Hypothesis naming.

commands() ​

php
static commands(
    mixed $initialModel,
    list<\ArbitraryInterface> $commandGenerators,
    int $minLength = 0,
    int $maxLength = 100,
): ArbitraryInterface

A valid StateMachine\Command sequence for stateful / model-based testing. Starting from $initialModel, each step draws a command generator and appends its command when the command's precondition holds in the running model, advancing the model — so the sequence is valid by construction. Shrinking drops individual steps and simplifies each command through its own tree. A sequence shorter than $minLength (no applicable command reached it) throws GenerationExhaustedException.

Feed the generated StateMachine\CommandSequence to StateMachine\StateMachine::check() in the property body, passing a factory that builds a fresh system under test.

rules() ​

php
static rules(
    class-string $machine,
    int $minLength = 0,
    int $maxLength = 100,
): ArbitraryInterface

A sequence of steps over a rule-based machine: one class whose #[Rule] methods are the steps, whose #[Invariant] methods hold after every step, and whose own fields are the model — commands() without a class per command:

  • $machine — The machine class: its #[Rule] methods are the steps.
  • $minLength — The fewest steps a sequence has.
  • $maxLength — The most steps a sequence has.

final class QueueMachine { private array $model = [];

public function __construct(private readonly Queue $sut) }

#[Rule] public function enqueue(int $value): void // drawn like a property's parameters { $this->sut->push($value); $this->model[] = $value; }

#[Rule] #[Precondition('notEmpty')] public function dequeue(): void { Assert::same($this->sut->pop(), array_shift($this->model)); }

public function notEmpty(): bool

#[Invariant] public function sizeMatches(): void { Assert::same($this->sut->size(), count($this->model)); } }

Gen::rules(QueueMachine::class)

The body calls StateMachine\RuleSequence::run() with a factory for a fresh machine, which checks the invariants and walks the steps — skipping one whose precondition is false in the machine's current state, running the rule and then every invariant for the others. An exception is the failed postcondition. Sequences are generated and shrunk exactly as commands() sequences are: steps dropped, arguments simplified. Rule parameters are drawn as forParameters() draws them, with overrides from a public static function <rule>Generators(): array or the method the attribute names. A machine with no rule, a rule that is not a public instance method, or a guard that does not exist is refused here, by name.

The StateMachine\Command interface stays the primitive for a machine whose model is a separate value; this is the shape for the common case where the model is a few fields.

sample() ​

php
static sample(
    \ArbitraryInterface<\TValue> $arbitrary,
    int $count = 10,
    int $seed = 0,
): array

Eagerly generate $count values from $arbitrary using a fixed $seed. A debugging aid for inspecting a generator's output and distribution; unlike the other factories it returns values, not an arbitrary.

sampleShrinks() ​

php
static sampleShrinks(
    \ArbitraryInterface<\TValue> $arbitrary,
    int $seed = 0,
    int $limit = 10,
): array

Eagerly generate one value from $arbitrary for a fixed $seed and collect its first direct shrink candidates. A debugging aid for authors of custom ArbitraryInterfaces: eyeball what the shrink tree offers before wiring the arbitrary into a property.