You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.