Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 18:25
· 36 commits to master since this release
fb592f5
  • The code both adapters carried byte for byte now lives here, so a DSN
    and a PROPERTY_* value mean the same thing under Testo and PHPUnit:
    Runner\CorpusFactory::fromDsn() (directory path or Redis DSN, memoized
    per value, ext-redis preferred, predis otherwise, any other scheme
    refused), Runner\Redis\RedisDsn and Runner\Redis\LazyPhpRedisCorpusClient,
    and Runner\EnvironmentOverrides with the parsers for PROPERTY_RUNS,
    PROPERTY_SEED, PROPERTY_PHASES, PROPERTY_EDGE_CASES and the flag and
    string variables. The engine still never reads the environment: the
    adapter reads a variable and hands the value over. PROPERTY_RUNS and
    PROPERTY_SEED past the integer range are refused instead of saturating
    to PHP_INT_MAX under a cast.
  • The Redis DSN has the shape everything else gives it (the IANA
    registration, predis, Symfony): redis://host[:port][/db][?prefix=key-prefix],
    rediss:// for TLS, an IPv6 literal in brackets. The path is the database
    index; the key prefix moved to the prefix query parameter. The pre-0.5
    form redis://host/suite-a: — the path as the prefix — is refused with the
    new spelling in the message rather than silently selecting a database. A
    refused connection or database is an error, no longer a corpus that is
    quietly empty.
  • A seed entry in the regression corpus now records the EdgeCases mode
    the failure was found under (edgeCases: mixin|none, read as mixin by
    documents that predate the field — the only mode there was), and the
    replay runs under that mode rather than the current configuration's. The
    modes share the roll but not the values it selects, so a suite that
    switched modes replayed other values, passed, and pruned a live
    regression. CounterExample carries the mode as $edgeCases, in
    toArray() too; Random exposes it as $edgeCases. The document format
    policy is now written down: within a format version the document grows
    only by optional fields, which older readers ignore and newer readers
    default; the version changes only when an existing field changes.
  • Shrinking accepts a candidate only when it fails with the same exception
    class as the original run. A smaller input that trips a different error
    (a TypeError in the body's setup below the assertion's boundary) used to
    be accepted, and the reported minimal counterexample minimised that other
    bug — the descent now stops at the boundary of the failure that was found.
  • Corpus::prune() finds a values entry recalled under a reordered
    signature: hydrate() accepts the reorder and hands the arguments back in
    the current order, so the re-encoded entry no longer matched the stored
    bytes and the fixed regression replayed on every run. Entry identity keys
    the arguments by name.
  • FilesystemCorpus reclaims an orphaned temp file: a writer killed between
    create and rename, under a pid that came around again (pid 1 in a
    container, a recycled range under paratest), left a regular file that made
    the exclusive create refuse every later write of that property for good.
    The write runs under the property's lock, so a regular file at the temp
    path can only be such an orphan; a symlink or a directory keeps the
    refusal.
  • Documentation: timeoutMs is measured when the run returns — it reports a
    run that overran, it cannot interrupt a body that hangs — and shrink
    trials are not timed; the README, llms.txt and the skill said or implied
    otherwise.
  • A shrink candidate that cannot be built no longer escapes the descent and
    discards the counterexample the random phase found. Shrinkable::map()
    skips a candidate the transformation refuses with an exception (a
    validating constructor under Gen::forClass() or Gen::map()), and
    Gen::flatMap() skips a source candidate under which the dependent side
    has nothing to generate — each together with its subtree, siblings still
    offered. As a safety net, PropertyRunner treats a candidate enumeration
    that throws (an Error in a transformation, a broken hand-written
    Shrinkable tree) as exhausted at that point: the candidates yielded before
    it count, the descent continues with the next node, and a replayed path
    indexing past the break reports the candidate as missing.
  • Gen::forClass() / Gen::forParameters() no longer fall back to the native
    type when a docblock type is outside the readable subset. float<0.0, 1.0>
    generated from float — the whole line for a parameter documented as the
    unit interval — is the widened guess the reader promises never to make; it
    is now an error naming the parameter and the documented type. The one
    fallback kept is a native class type, whose docblock can only narrow
    generics (Collection<Item>), never the values.
  • Docblock types now resolve class names: list<LineItem>, Status|null,
    'draft'|'published'|null, \DateTimeImmutable and a namespace-relative
    Order\LineItem are read the way the code beneath the docblock reads
    them — through the declaring file's namespace and use imports (aliases
    and group imports included), which reflection does not expose, so the
    file is tokenised once. ext-tokenizer is now required. Literal unions
    accept null, true and false beside quoted strings and integers.
  • Gen::forClass() / Gen::forParameters() reject an override whose key
    names no parameter (['amout' => …]) instead of silently generating the
    parameter from its declared type.
  • Gen::regex() / stringMatching() bound the longest string a whole
    pattern can generate (10,000 characters) at compile time, not only each
    quantifier on its own: (a{10000}){10000} and a deeply nested (a*)*
    chain passed the per-quantifier guard and exhausted memory on the first
    draw. maxRepeat above the same bound is rejected for the same reason.
  • FloatArbitrary (Gen::floatBetween()) no longer emits the exclusive
    upper bound when the span is within a few ulps of min (1e16 .. 1e16 + 2
    rounded up to max), and rejects a NAN/INF bound instead of generating
    NAN forever.
  • Lists, unique lists, maps, strings, charset strings and byte strings now
    shrink by removing contiguous blocks of elements from every position —
    the whole sequence, then aligned halves, quarters, …, single elements —
    before shrinking elements in place, the way QuickCheck's shrinkList and
    Hedgehog do. The previous length phase only halved prefixes, so a failing
    element in the middle or at the end could never be isolated: [7, 3, 42]
    under "no 42 allowed" stopped at [0, 0, 42], and now reaches [42].
    Generation is unchanged (same seed, same values); a shrink path recorded
    against a sequence generator before this release indexes different
    candidates and is reported as stale by path replay.
  • Gen::oneOf() / OneOfArbitrary accept a string-keyed variadic (named
    arguments, a spread map) — the values are picked by position instead of
    reading a missing index 0.