Skip to content

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 18:17
· 67 commits to master since this release
ae0b329
  • 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 accepts Gen::oneOf(),
    Gen::elements(), Gen::frequency() and Gen::commands() through the new
    Swarmable interface, which a custom choice generator can implement too;
    surviving frequency branches 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; swarming Gen::commands() with a non-zero
    minLength can starve the sequence and throw GenerationExhausted, exactly
    as an unrestricted generator starved by its model does.

  • Added the machine-readable distribution report. PropertyFinished now carries
    a DistributionReport: every Classify label as a LabelShare (count, share
    and the cover() 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
    coverageAssessed says 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. RunStatistics carries the cover() 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 on CounterExample::$path (and in toArray() /
    toJson()) as name:index steps, where a step names a parameter — or an
    in-body draw under its draw#N pseudo-name — and the shrink candidate that
    was accepted. Passing it back through PropertyConfig::$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 new PathFailed result 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 without Random or Shrink, shrinking
    switched off, a wall-clock shrink budget, a maxShrinks below 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::Off reports a counterexample exactly as generated (no trial, no
    shrink event); shrinkBudgetMs bounds the descent by wall clock and keeps
    the best candidate it reached, which maxShrinks could 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 Phase and PropertyConfig::$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 without Phase::Shrink is exactly
    ShrinkMode::Off; Phase::Corpus gates replay only, and a fresh
    falsification is still recorded; without Phase::Random the 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 a Phase is 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.txt and 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 inside it()), why id() is not optional there,
    and why no it(...)->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 with Classify::label('42') came back from the per-run buffer as
    42 and travelled on to RunPassed::$labels — declared list<string>. The
    label is now handed back as the string the body recorded, and the internal
    counters declare the array-key they can actually hold instead of a string
    they cannot.