Repository navigation
v0.2.0
-
Added
Gen::swarm()— swarm testing over a choice generator. Each generated
case may use only a random, non-empty subset of the wrapped generator's
variants, so the cases that never perform an operation at all stop being
astronomically rare: over 200 eight-command sequences, 4 avoided one command
by chance against 77 when swarmed. It acceptsGen::oneOf(),
Gen::elements(),Gen::frequency()andGen::commands()through the new
Swarmableinterface, which a custom choice generator can implement too;
survivingfrequencybranches keep their weights. Shrinking stays inside the
subset a case was drawn from and never widens back to the full alphabet —
without that, a counterexample found without some operation would shrink into
one containing it, and the finding would stop reproducing. The subset is
drawn once per generated value; swarmingGen::commands()with a non-zero
minLengthcan starve the sequence and throwGenerationExhausted, exactly
as an unrestricted generator starved by its model does. -
Added the machine-readable distribution report.
PropertyFinishednow carries
aDistributionReport: everyClassifylabel as aLabelShare(count, share
and thecover()threshold it was registered with), the discard tally, and
toArray()for telemetry — the contents of the line an adapter prints, before
it becomes a line, so a CI job or a test can read it without parsing text.
Label shares are over the successful checks and the discard share is over the
attempts, named apart so the two cannot be confused; a label that was required
and never occurred is reported with a count of zero rather than omitted; and
coverageAssessedsays whether the run reached the coverage assessment at
all, so a report never implies a verdict that a run which gave up or exhausted
its budget never reached.RunStatisticscarries thecover()requirements
alongside the counts they are compared against, at every exit that builds one.
A falsified run carries no report — it stops at the counterexample. The report
is a projection of counters the phase already accumulated, computed once when
the run finishes; printing stays with the adapters. -
Added shrink-path replay. A falsified property now reports the descent that
produced its counterexample onCounterExample::$path(and intoArray()/
toJson()) asname:indexsteps, where a step names a parameter — or an
in-body draw under itsdraw#Npseudo-name — and the shrink candidate that
was accepted. Passing it back throughPropertyConfig::$path, together with
the seed it came from, follows those steps instead of searching for them
again: one body execution per accepted step instead of one per candidate
tried. It does not skip the random phase; reaching the failing run still
means executing the runs before it. A path is a debugging aid rather than a
fixture — its steps index into shrink candidates, so editing a generator
orphans it, which is what the regression corpus is for. A path that no longer
applies is reported as the newPathFailedresult carrying the new
PathViolationException, naming the step that broke, and is never absorbed
into a fresh search. Configurations that would leave the path a silent no-op
(no explicit seed, a phase set withoutRandomorShrink, shrinking
switched off, a wall-clock shrink budget, amaxShrinksbelow the path's own
length, a malformed path) are rejected at construction. The failure message is unchanged: the path travels on the
counterexample, and printing it is the adapters' half of the 0.2 line. -
Added
PropertyConfig::$derandomize: with it set, a run without an explicit
seed derives one from the property's id instead of drawing it at random, so
the same property on the same code always selects the same inputs. The
regression corpus only helps once a failure has been recorded; this covers
the other side of that moment — a bug found locally reproduces in CI before
any corpus entry exists, and a property that passes (and therefore records
nothing) keeps a stable input distribution, which is what makes distribution
numbers comparable between commits. An explicit seed still wins, and the
mapping from a seed to the values it produces is untouched. -
Added shrink modes and switchable run phases to
PropertyConfig.
ShrinkMode::Offreports a counterexample exactly as generated (no trial, no
shrink event);shrinkBudgetMsbounds the descent by wall clock and keeps
the best candidate it reached, whichmaxShrinkscould not do because it
counts accepted steps rather than the tried candidates a descent actually
spends its time on. A shrink budget deliberately trades determinism for a
bounded descent — the corpus and an explicit seed remain the reproducible
paths. A budget too large to convert into its own nanosecond deadline is
rejected: an overflowed deadline is not a deadline. -
Added
PhaseandPropertyConfig::$phases: the stages of a run (examples,
corpus replay, random, shrink) are now a set instead of a fixed sequence, so
a pull request can replay only the corpus and the pinned examples, and a
property can be measured with corpus replay off without deleting the corpus
to do it. An empty set throws; a set withoutPhase::Shrinkis exactly
ShrinkMode::Off;Phase::Corpusgates replay only, and a fresh
falsification is still recorded; withoutPhase::Randomthe result carries
honest zero statistics and no coverage assessment, and passes only once the
enabled earlier phases have. A phase set is validated element by element: a
value that is not aPhaseis rejected rather than silently skipped, since
an unrecognised stage would make a property report green having checked
nothing. -
Added
Gen::ipv6(): IPv6 address strings in the canonical text form of
RFC 5952 — lowercase hex, leading zeros stripped, 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 zero, so the descent walks the
shortened forms address parsers get wrong and ends at::. IPv4-mapped
addresses, zone ids and the bracketed URL form are out of scope;Gen::url()
still emits no IPv6 host. -
Documented
rasuvaeff/property-testing-names— the person-name domain
generator built on this engine — in both READMEs,llms.txtand the
bundled skill. -
Documented two things the site never covered: a cookbook comparison
Faker vs property,
which runs one UTF-8 truncation bug past a realistic-data generator and a
shrinkable one and quotes what each reports (both falsify; one shrinks 30
steps to another arbitrary name, the other one step to the boundary), and a
Pest section on the PHPUnit adapter page — the scenario that already works
(uses()plus the chain insideit()), whyid()is not optional there,
and why noit(...)->forAll(...)chain exists or is planned. -
Added
PropertyId::unstableWarning(): the warning text for a property id
derived from a closure (Suite::{closure}on PHP 8.3,
Suite::{closure:file:line}from 8.4), or null when the id is stable. Such an
id breaks the regression corpus without breaking anything visible — on 8.3
every closure of a class shares one key, so two properties in a file overwrite
each other's counterexample; from 8.4 the key carries a line number that any
edit above shifts, orphaning yesterday's entry. The engine returns the
sentence and prints nothing; an adapter shows it through the channel it
already warns on. -
Fixed a numeric classification label reaching listeners as an int. PHP stores
a numeric string such as'42'under an integer array key, so a label
recorded withClassify::label('42')came back from the per-run buffer as
42and travelled on toRunPassed::$labels— declaredlist<string>. The
label is now handed back as the string the body recorded, and the internal
counters declare thearray-keythey can actually hold instead of astring
they cannot.