Skip to content

Releases: rasuvaeff/property-testing-core

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 02 Oct 19:48
2b5fda1
  • Added: #[Generate] — a parameter attribute holding its generator, read by
    Gen::forParameters() and Gen::forClass() after an override and before the
    docblock and the native type (#160). It takes an arbitrary built with new, or
    a reference to a static factory returning one ('method', 'Class::method',
    [Class::class, 'method'], an invokable) — the reference is how a generator
    built with a closure (map, email, regex) reaches a parameter on PHP 8.3
    and 8.4. A broken attribute is an exception naming the parameter.
  • Added: Gen::forParameters(derive: false) reads only overrides and
    #[Generate] and refuses a parameter with neither — the engine half of an
    adapter's mode without auto.
  • Added: the docblock reader understands numeric-string,
    non-falsy-string/truthy-string, lowercase-string,
    non-empty-lowercase-string and array shapes — array{a: T, b?: U},
    quoted and integer keys, positional array{T, U} and list{T, U}, array{},
    shapes nested in collections and unions (#156). An optional key is sometimes
    absent and shrinks towards absent. Unsealed shapes and double-quoted keys stay
    refused. A @param type written across several lines is now read.
  • Changed: refusals about a parameter now end with
    pass an override or #[Generate].

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 08:31
5d6e5f2
  • Changed: a shrink candidate is accepted only when it fails the same way
    the original run did — the same exception class and the same place in the
    property's own file. A body that can fail in more than one place (two
    assertions, or an assertion beside a call that throws the same type) used to
    offer the descent failures it could not tell apart, so the reported minimal
    counterexample could belong to a bug the run never found, with nothing in the
    output saying so. The place is the line of the property's own file in the
    failure's stack, which is not where the exception was raised: an assertion
    library raises every assertion of a body from one line of its own, while the
    line that called in is the assertion the body wrote. Where the place cannot be read —
    an id that names no loaded class, a body written in another file (a trait, an
    included fixture), a failure the engine raised rather than the body — the
    exception class stays the whole criterion. Minimised counterexamples may
    therefore differ from 1.0.0 on such a body (compatibility policy §2: what a
    descent minimises to is not under SemVer); a corpus recorded before this
    release replays unchanged. Worth knowing for stateful properties: the
    rule-based façade's #[Rule] methods live in the test file, so a command
    sequence whose failure moves from one rule to another now stops the descent
    rather than being minimised into the other rule's bug.

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 20 Sep 08:53
b8f2845

The stability release. The @api surface is that of 0.12.0, unchanged: no
type, method or parameter was added, removed or renamed, and a project on
^0.12 upgrades by widening the constraint to ^1.0. What the number
promises from here on is written out in the compatibility policy
(README.md, docs/src/guide/compatibility.md): the @api surface, the
corpus format (FilesystemCorpus::FORMAT_VERSION does not change within
1.x), the machine-readable keys of CounterExample::toArray() and
DistributionReport::toArray(), event fields and order, append-only
constructors — while seed → values stays outside SemVer, fenced by
FilesystemCorpus::SEQUENCE_EPOCH. The Gen::* parameter names are frozen
as they stand: $minLength/$maxLength for strings, bytes and sequences,
$minSize/$maxSize for collections, $min/$max for ranges.

  • The adapters (rasuvaeff/property-testing-testo,
    rasuvaeff/property-testing-phpunit) and rasuvaeff/property-testing-names
    release their own 1.0.0 requiring this package as ^1.0, in that order
    (policy §8).
  • Docs: the version gates that dated each feature (core ≥0.12,
    -testo ≥0.6, -phpunit ≥0.5, …) are gone from the skill, the guides and
    the migration document — every 1.0 install has all of them. The migration
    recipe requires -testo:^1.0.
  • composer rector is green again: the three seed-hunting loops in
    CompositeArbitraryTest share one firstGenerated() helper instead of a
    nullable sentinel that FlipTypeControlToUseExclusiveTypeRector rewrote.

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 20 Sep 03:27
02989f0
  • Added: Gen::uniqueArrayOf(..., by:) — uniqueness by the int|string
    key a closure returns per element, for value objects unique by one field;
    a key of any other type is refused at generation time rather than compared
    by identity. Value-based uniqueness is unchanged, sequence and all (#146).
  • Added: Gen::withEdgeCases($inner, ...$edgeCases) — per-generator
    boundary values: one draw in five is one of them, they come first in the
    shrink order, and the bias stays on under EdgeCases::None. The wrapper
    rolls on the run's randomness and leaves the inner sequence untouched, so
    the sequence epoch does not move (#144).
  • Added: Gen::note($label, $value) — a computed value attached to the
    run, carried on the counterexample as $originalNotes/$shrunkNotes and
    rendered as a Notes: line for the minimised run. CounterExample::toArray()
    gains the two keys, always present (#142).
  • Added: Classify::tabulate($table, $tags) — per-run categories a run
    may belong to several of, tallied per table with the pairwise intersections
    of tags hit together, on RunStatistics/DistributionReport::$tables and
    $intersections; toArray() carries a tables key only when a run
    tabulated (#149).
  • Added: Gen::randomEngine() / Gen::randomizer() — a Random\Engine
    (and the Randomizer over it) whose output is drawn from the property's
    tape, so code that takes a randomizer has its random decisions recorded,
    replayed and shrunk toward "\0"; Gen::forParameters() derives both from
    the native parameter types (#141).
  • Added: Gen::composite(Closure(Draw): T) — several dependent draws in
    one reusable generator, shrinking the draws earliest first with later draws
    re-drawn through the new range from a stream of their own position; the
    descent is capped like in-body draws (#148).
  • Added: exhaustive mode — PropertyConfig::$exhaustive /
    $exhaustiveBudget walk the whole parameter product instead of sampling
    when every generator implements the new Enumerable seam
    (domainSize()/enumerate(): constant, bool, int, oneOf/elements/enum,
    nullable, tuple, record, map, filter, withEdgeCases) and the product fits
    the budget; the walk is seed-independent, and RunStatistics/
    DistributionReport report $domainSize or $exhaustiveDeclined (#147).
  • Added: flaky detection — PropertyConfig::$flakyReplays (default 2)
    re-executes the minimised counterexample; a replay that passes marks it
    flaky (CounterExample::isFlaky(), $replays, $passedOnReplay, a
    Flaky: line in the message). No new events; wall clock only (#145).
  • Added: rule-based stateful façade — #[Rule], #[Precondition],
    #[Invariant] on one machine class, Gen::rules($machine) producing a
    RuleSequence of RuleSteps over the existing command-sequence generation
    and shrinking; RuleSequence::run($factory) builds a fresh machine per run
    and keeps the factory out of the value (#143).
  • Added: targeted search — Target::maximize()/minimize() report a
    score, PropertyConfig::$searchRuns opens a hill-climbing phase after the
    random one (one parameter regenerated at a time over a pool of the best
    inputs), TargetImproved announces every new best, SearchReport on
    PropertyFinished/RunStatistics sums it up, and the new SearchCorpus
    seam (implemented by FilesystemCorpus and RedisCorpus, in a separate
    search document) keeps the pool between runs. Measured on a corner bug at
    equal budget: 52 → 98 of 100 seeds; no gain, no loss where the score does
    not lead to the bug (#150).
  • PropertyFinished gains a fourth constructor parameter, $search;
    RunStatistics gains $tables, $intersections, $domainSize,
    $exhaustiveDeclined, $search; DistributionReport the matching
    fields; CorpusFailed::$operation may now be recallTargets or
    rememberTargets. All appended with defaults.
  • Added: EnvironmentOverrides::count($variable, $value) — a
    non-negative integer for count-shaped variables such as
    PROPERTY_SEARCH_RUNS, where 0 is a valid "none".
  • The adapter contract suite pins the path version at 0.11.0, which both
    adapters accept; it sat at 0.10.0.
  • A refusal over a docblock type no longer calls a generic the function or
    its class declares (@template T) an unknown class: list<T> is reported
    as unreadable, and only a name nothing declares as unknown class "…".
  • docs/.api-workspace moves its path version for core to 0.11.0 and the
    adapter constraints to -testo ^0.11 / -phpunit ^0.9, the bridged
    releases (0.11.1 / 0.9.1) that accept core 0.11.

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 19 Sep 11:10
1987888
  • Fixed: RedisDsn::parse() quoted the password when the DSN carried
    userinfo but no host (redis://user:s3cret@, redis://user:s3cret@:6379):
    parse_url() returns false there, and the userinfo check trusted its
    answer. Credentials are now looked for in the raw authority before anything
    else is read, so the "carries credentials" message is the one you get, and it
    never echoes the DSN (#128).
  • Fixed: floatBetween($min, $max) shrank to $max when the range lay at
    or below zero — the one value the generator never produces. The shrink target
    is the point of [$min, $max) nearest to 0.0: for such a range the largest
    float under $max. Generation is unchanged; SEQUENCE_EPOCH stays (#129).
  • Fixed: FilesystemCorpus no longer skips a write in silence. A lock file
    replaced by a symlink, an occupied temp path, a short write, a failed rename
    or a document held by another format version now throw RuntimeException,
    which the runner reports as CorpusFailed; CorpusStored is emitted only
    once the document is on disk. The property's outcome is unaffected, as
    before (#136).
  • Fixed: PropertyRunner::run() dropped listeners from an iterable that
    repeated keys — iterator_to_array() kept the keys (#134).
  • PropertyTestingException: an empty marker interface (extends \Throwable)
    implemented by all ten @api exceptions, so catch (PropertyTestingException)
    catches whatever the engine reports. AssumptionSkipped stays outside it on
    purpose — a discard is control flow, not a failure (#130).
  • AssumptionSkipped is @api: every TrialExecutor has to catch it to map
    Assume::that() to TrialOutcome::discarded(), and the executor seam is
    documented as such (#131).
  • Gen::forClass() / Gen::forParameters() read @psalm-param and
    @phpstan-param (winning over @param) and the @var written on a promoted
    constructor property when the constructor docblock says nothing about it.
    /** @var int<0, max> */ public int $amount no longer generates from the
    native int (#132).
  • Gen::stringOf() defaults its bounds to 0..100, like stringFrom() and
    bytes() (#133).
  • new FilesystemCorpus('') and a Redis DSN with a #fragment are refused with
    InvalidArgumentException instead of writing under / or dropping the
    fragment (#135).
  • Refusal messages name what they refuse: a native union or an untyped
    parameter is reported as typed int|string / untyped rather than "with no
    usable type", an unknown class inside a docblock type is named (unknown class "Nope"), and an enum without cases or Gen::elements([]) says which
    enum or argument is empty instead of "OneOf requires at least one value".
  • Documentation: llms.txt and the README tables name the Gen::* parameters
    as the signatures do ($minSize/$maxSize, $map, $flatMap,
    $predicate, $arbitrary); the frozen keys of CounterExample::toArray()
    and DistributionReport::toArray() are listed and pinned by a test; a corpus
    seed entry replaying without Phase::Random, timeoutMs not judging
    discarded or skipped runs, the engine never warning about the discard share,
    a native float meaning floatBetween(-1e6, 1e6), dictOf redrawing
    integer-like string keys, floatBetween($x, $x) and NAN under
    uniqueArrayOf are written down. composer-require-checker.json is
    export-ignored.
  • docs/.api-workspace moves its path version for core to 0.10.0 and the
    Testo adapter constraint to ^0.10, now that the adapter's ^0.9 || ^0.10
    bridge is released (0.10.0). The 0.10.0 release notes explain why the path
    version had to stay at 0.9.0 for a day.

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 12 Sep 18:25
df01298

The contract freeze ahead of 1.0. Everything here is something that could not
be changed after 1.0.0 without a major, so it is changed now. The new
Compatibility policy says what the numbers
will promise from then on.

  • Breaking: CounterExample::$skips is now $discards, and $skips is a
    real, separate counter — the runs the environment refused. The two were one
    word for two quantities: the field held Assume::that() discards, while
    RunStatistics::$skips (0.9.0) held environmental skips, in the same @api.
    toArray()/toJson() gain a discards key beside skips. The corpus is
    untouched — CorpusDocument never wrote the full toArray().
  • Breaking: FilesystemCorpus::fromEnv() is removed. It read PROPERTY_DB
    and built new self($directory) unconditionally, so PROPERTY_DB=redis://…
    created a directory literally named redis:/host:port/… and emitted four
    E_WARNINGs per call from the stream-wrapper lookup — a silent fall back to
    the filesystem, which is exactly what the family forbids. Use
    CorpusFactory::fromDsn(EnvironmentOverrides::string(getenv('PROPERTY_DB')) ?? …),
    which cannot return a backend the value did not name. Nothing in src/ calls
    getenv() any more.
  • Breaking: GenerationExhausted is GenerationExhaustedException and
    StateMachine\PostconditionViolation is
    StateMachine\PostconditionViolationException, matching the seven public
    exceptions that already carried the suffix. No aliases: 0.x is where a rename
    costs a line in an upgrade note rather than a major.
  • Breaking: every Gen::* factory returns ArbitraryInterface<T> instead
    of a concrete arbitrary. The concrete classes stay @api and unchanged —
    they are simply no longer part of the facade's signature, so an
    implementation can be replaced without a major. Gen::enum() bounds its
    template at \UnitEnum and Gen::intRange() declares the honest
    array{int, int} it produces.
  • Breaking: a flag environment variable is off for 0, false, off and
    no (case-insensitive, trimmed), not only 0 and ''.
    PROPERTY_VERBOSE=false turning verbose output on was 2.8-compatible and
    indefensible.
  • Breaking: left implicit, the skip budget is runs rather than the
    discard budget's runs * 10. A property skipped on every run gives up after
    runs + 1 attempts instead of 10 * runs + 1 — under Testo that is a
    #[BeforeTest] hook executed 101 times rather than 1001 for a runs: 100
    property. An explicit maxDiscards still governs both budgets; transient
    skips still do not end a property early.
  • RunDiscarded carries bool $skipped, so a listener can tell an
    environmental skip from an Assume::that() discard. Both adapters'
    VerboseListener counted them as one.
  • DistributionReport carries int $skips and reports it in toArray().
    discardPercent() keeps dividing by attempts — that denominator is the
    point of the name.
  • GaveUpException carries ?int $maxSkips, the cap the skip budget was
    measured against; the skip message names it. Null means the caller did not
    tell the two budgets apart, and the message falls back to $maxDiscards.
  • Redis AUTH is reachable: RedisDsn and CorpusFactory::fromDsn() take an
    optional password, LazyPhpRedisCorpusClient authenticates before SELECT,
    and toPredisParameters() carries it. Adapters read PROPERTY_DB_PASSWORD.
    The DSN's userinfo is still refused — PROPERTY_DB is echoed in diagnostics
    and lands in CI logs.
  • RedisCorpus refuses an empty prefix and a negative maxValues/maxSeeds.
    A negative cap made CorpusDocument::cap() drop every entry in silence.
  • composer rector is green: PropertyRunner and RegexCompiler name two
    literal arguments. It had been red since 0.9.0, which is composer release-check red — composer build does not run rector.
  • Documentation is part of the contract, and this release closes 37 places
    where it disagreed with the code: the split Configuration table in both
    READMEs, forAll($generators, $id) (the id comes from ->id()),
    floatBetween's interval (half-open, and it had no description at all), the
    four-of-eleven examples tables, the four-of-nine environment variables page,
    the eight-of-fifteen attribute table, Command::shrinks() and minLen,
    PSR-20 as the adapters' clock, path "when it ships in 0.2", CorpusFromEnv,
    ext-tokenizer missing from the requirements, and Redis absent from the
    regression-corpus page entirely. The site gained a Compatibility policy
    page, roadmap.md no longer promises a seed sequence that 0.6.0 changed, and
    the API reference reflects traits — so the PHPUnit adapter's single entry
    point, the PropertyTesting trait, has a page for the first time.
  • docs/.api-workspace follows the adapters again (^0.9/^0.7; it sat at
    ^0.6/^0.5, and a caret on 0.x pins a minor, so the weekly docs rebuild
    had been re-reflecting the same stale release). Keeping it in step is now in
    the release checklist beside build.yml's pin. At release time build.yml
    moves to 0.10.0 and the workspace to ^0.9/^0.8, but the workspace's
    path version stays 0.9.0 for now: the published Testo adapter still
    constrains core to ^0.9 (its ^0.9 || ^0.10 bridge is merged, not
    tagged), and a 0.10.0 path version would make the workspace uninstallable.
  • The Compatibility policy gains the point the 1.0 review asked for and the
    freeze first left out: how data is reached is frozen as it stands. The
    accessor style is not uniform — results, events and most exceptions expose
    public readonly properties, while PropertyViolationException,
    ExampleViolationException, PathViolationException and
    RegressionViolationException expose getters — and it stays that way. Unifying it would break every consumer for a cosmetic
    gain, and the split is at least stable: a type either has the field or the
    getter, and one never becomes the other. Written down rather than left to be
    rediscovered.
  • ValueCodec refuses an int-flagged array key whose text is not a decimal
    integer the platform holds: a bare (int) cast saturated 9223372036854775808
    to PHP_INT_MAX and collapsed abc to 0, replaying a key the property was
    never called with. The lenient 007 → 7 reading stays (#119).
  • ValueCodec reads a finite float back only in the shape var_export() writes
    it: 1e999 (cast to INF), 1.0E-400 (cast to 0.0), 01.0, .5 and
    whitespace-padded texts were accepted by is_numeric() and decoded to a
    value the encoder could never have stored (#120).
  • FrequencyArbitrary reports weights whose sum passes PHP_INT_MAX as an
    InvalidArgumentException; the float sum used to reach the typed property
    and fail with a TypeError (#121).
  • PropertyDefinition rejects a repeated parameter name — the runner combined
    names and draws by name, so one draw was dropped without a word and the
    body received fewer arguments than declared (#122).
  • DateTimeArbitrary refuses a bound outside the microsecond range of a
    64-bit integer (about 292 000 years either side of the epoch) with an
    InvalidArgumentException; the overflowed product used to fail the int
    return type with a TypeError (#123).

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 09:17
47985cc
  • Breaking: Gen::oneOf() and Gen::elements() reject an
    ArbitraryInterface among their values. oneOf(generator, generator) is how
    fast-check, jqwik and Hypothesis spell "pick one of these generators", and
    here it made the generator objects themselves the data: the body received
    IntArbitrary and StringArbitrary instances, never a generated value, and
    the property reported green having checked nothing. Pick between generators
    with Gen::frequency(); oneOf/elements take values.
  • Breaking: Gen::regex() / Gen::stringMatching() reject a delimited
    pattern. The compiler always wanted [a-z]{3,6}, not /[a-z]{3,6}/, and the
    word "delimiter" appeared nowhere in the code or the documentation. A PHP
    developer arrives with a pattern from preg_match(), which is always
    delimited, and got one of two things: an anchored pattern was blamed on its
    anchor (the ^ is not leading only because a / precedes it), and an
    unanchored one compiled with the delimiters as literals — values that still
    passed an unanchored preg_match(), so the property stayed green while every
    generated string carried two junk characters. The rejection names the
    undelimited pattern to paste back, and escaping the character still matches
    it literally.
  • Environmental skips no longer spend the discard budget, and
    RunStatistics::$skips counts them apart from $discards. A skip says
    nothing about the input, so charging it to the discard budget made a machine
    missing a dependency give up — and be advised to narrow generators that were
    never at fault. GaveUpException gained $skippedRuns and
    $exhaustedBySkips, and reports the environment rather than the generators
    when it is the skip budget that ran out. Both budgets share the
    maxDiscards cap.

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 17:32
852a338
  • TrialOutcome::skipped() joins discarded() as a third "this run checked
    nothing" outcome, and adapters should report an environmental skip
    (markTestSkipped(), a framework skip from a lifecycle hook) as such. It
    counts as a discard everywhere but one: a recorded regression whose replay
    only skipped is kept instead of pruned. Before, one machine without the
    dependency the body guards against deleted the counterexample for every
    machine that has it.
  • A shrink-path replay no longer accepts GenerationExhausted as the step's
    falsification. A draw past the end of the recorded tape regenerates through
    the live generator, and an exhaustible one (filter(), uniqueArrayOf()
    with a minimum, commands() with a minimum length) can fail to produce a
    value: the recorded bug was reported as a generator exhaustion. The step is
    now reported as a stale path.
  • prune() says so — as a CorpusFailed event — when the entry cannot be
    re-encoded to the key that identifies it, on both the filesystem and the
    Redis backend. It used to compute a null key, match nothing and silently
    replay the entry forever.
  • Gen::forParameters() / Gen::forClass() refuse a DateTimeInterface type
    that is not DateTimeImmutable itself — a subclass, or DateTime — by name.
    Reflection over the inherited constructor asked for a string and blew up
    with a date-parse error deep inside the recursion instead.
  • A Redis DSN with an array-valued prefix (?prefix[]=x) is refused rather
    than silently falling back to the default prefix, and a database index with a
    leading zero (/01) is reported as the spelling mistake it is instead of as
    a number outside the integer range.
  • The corpus file is read through a suppressed file_get_contents(), like
    every other filesystem call in that class: a file removed between the
    is_file() check and the read no longer prints a raw warning into the
    suite's output.

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 15:52
ec08fff
  • PropertyConfig rejects a timeoutMs or budgetMs past the value where the
    millisecond-to-nanosecond conversion leaves the integer range, the way it
    already rejected such a shrinkBudgetMs. Above that bound the deadline
    silently stopped being one.
  • A corpus document whose keys collide once PHP normalises them — 0 and "0"
    in one encoded array, which no real PHP array can hold — is refused instead of
    losing a pair to array_combine().
  • Gen::regex() strips a trailing $ anchor by the parity of the backslash run
    before it, so foo\\$ compiles (literal backslash, real anchor) instead of
    failing on the anchor it mistook for an escaped $.
  • A quoted literal in a psalm docblock type may contain the separator of the
    type it sits in: 'a|b'|'c' and array{k: 'x,y'} are read as written, and
    \' inside a literal is unescaped.
  • Gen::oneOf() enumerates shrink candidates for objects by identity. The
    previous var_export() key threw on a cyclic object graph and treated two
    distinct objects of equal state as one candidate.
  • Gen::forClass() builds its ReflectionClass once instead of once per
    generated value.

v0.6.1

Choose a tag to compare

@github-actions github-actions released this 03 Sep 17:55
2296656
  • Redis corpus DSNs accept a finite connection timeout (default 5.0),
    passed to both Predis and ext-redis clients.
  • Redis DSN ports and database indexes are validated strictly; invalid and
    overflowing values are rejected instead of being silently changed.