v0.2.0
New verbs and richer failure messages; nothing removed, nothing renamed. A
minor rather than a patch because expectSequence() is new public API, and on
0.x that is the boundary Composer's caret already treats as breaking.
-
Understudy::expectSequence()/expectSequence(): a protocol armed before
the subject runs.ordered()andverifySequence()both answer in
teardown, where the stack trace points atverifyAll()rather than at the
call that went out of turn. An armed protocol refuses inside the offending
call, with the subject's own frame on top of the stack. Totality is scoped to
the doubles the protocol names: on those, a call is the step due or something
the test configured, and anything else is refused — so a query the subject
makes between two steps has to be stubbed. Each step is due exactly once, in
order. Arming is also a claim:verifyAll()reports the steps the subject
never reached, which is what still fails a test whose subject swallowed the
refusal in a broadcatch. One protocol at a time, a finished one may be
replaced, andcheckpoint()verifies it and then drops it. The failure is a
VerificationFailedcarryingFailureKind::OutOfSequence— no new kind, so
an exhaustivematchon the enum keeps compiling. -
A strict double says what it refused and what it compared the call
against. Naming only the method sent the reader back to a test that had
configured that method — the difference was in the arguments, and the message
did not carry them. The refusal now renders the call and every expectation
registered for that method that did not accept it, with each rejecting
argument marked from the expectation's side, a position the call never
carried included. Up to five, in the order the dispatcher tried them, then a
count. With nothing configured for the method there is nothing to compare
against and the message is unchanged. A matcher asked while the message is
built is asked defensively: it runs inside the code under test, and one that
throws counts as one that did not accept rather than replacing the refusal
with its own exception. -
Fixed a deprecation introduced with object rendering:
SplObjectStorage::contains()
is deprecated as of PHP 8.5, and every rendered object emitted a notice. -
Performance. Four per-call and per-double costs came off the hot paths:
the armed-protocol check now sits behind one null check, a context is recorded
live once where it is created rather than once per double adopted into it, and
oneReflectionClassis built per generated class rather than per double. A
fifth change — retiring a context by marking it instead of walking its
doubles — was measured, found to move the cost onto creation (13-23% worse in
the benchmark) rather than remove it, and reverted; the finding is recorded in
AGENTS.md. -
perf/README.mdre-measured on a quiet machine. The previous figures were
taken at a commit before 0.1.0 and described no released version. They also
did not show a creation regression that landed before the first tag and
shipped in 0.1.0, 0.1.1 and 0.1.2; it is bisected, documented, and still
present. -
An object argument renders as an alias and its public state, so the
*
that marks a differing argument points at something the reader can see:
save(App\Book#1 {title: 'Dune'})againstsave(*App\Book#2 {title: 'Dune'}*)says the call was made with a rebuilt copy rather than the
instance the expectation named. Aliases are numbered within one message in
order of first appearance — not object ids, which are reused after a
collection and would print differently on each run — and the same instance
keeps one number everywhere in that message. The braces list public
properties only, up to five, at the message's existing depth budget: nothing
is called to render them, so an object keeping its state behind getters
renders as its alias alone. Failuresummarytext changes; the structured
fields ofVerificationFailuredo not.