From d98559bd8ac1bb89ffda621b75bdb0bbd8da86fd Mon Sep 17 00:00:00 2001 From: "v.razuvaev" Date: Sat, 5 Sep 2026 16:15:23 +0300 Subject: [PATCH] =?UTF-8?q?Release=200.9.0:=20the=201.0=20candidate=20?= =?UTF-8?q?=E2=80=94=20freeze=20decisions,=20parity=20rows,=20docs=20catch?= =?UTF-8?q?-up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every decision of the 1.0-readiness review lands here, so the tag after this one can be 1.0.0 without a change to the surface. Breaking on 0.x, and a minor for that reason: - forget() on a retired or reset double answers ForgottenDouble, like every other facade (it went through stateOf() now); it said the object "is not one". - One rule for the two specification exceptions: shape -> InvalidCallSpecification, value -> InvalidSpecificationArgument. invertedBounds() and invalidPattern() move to InvalidSpecificationArgument beside unknownType(); tests, docs and check-claims follow. - Outcome and Cardinality are @internal (no public path accepts or returns either); Invocation::recordOutcome() and __construct() are @internal; WhenBuilder is closed by contract in its docblock (a @final tag would make Psalm refuse ExpectBuilder); FailureKind growth policy written down. - Docblocks the API reference renders: checkpoint() spans every live context, verify() documents $never, expectSequence() loses its lone @api tag, Arg::bool(), WhenBuilder::throws(), Invocation readers and seven exception factories get summaries. docs/src/api/** regenerated; completeness budgets lowered to the new measurement. - bin/consumer-smoke: five parity idioms (CardinalityAfterVerb, VoidReturnsChain reported; NamedBoundsSwapped, HelperReceiver, FirstClassCallable silent). - SKILL.md caught up with the engine (nullable returns, four functions, allOf/ anyOf/rest/captor, expectSequence, lean, delegate); README ×2, llms.txt and eight site pages carry the 0.8.0/0.3.0/0.2.0 corrections they had missed. - Rector green again (red since v0.7.0); infection ^0.35; property-testing-testo ^0.9; .vale.ini export-ignore; mutation gate stays 92 with the measurement (94.1%, three mutants above 94) recorded beside the number. --- .gitattributes | 1 + AGENTS.md | 9 +- CHANGELOG.md | 75 ++ README.md | 23 +- README.ru.md | 22 +- bin/consumer-smoke | 136 ++- composer.json | 4 +- docs/scripts/api-snapshot.json | 781 +++++++++--------- docs/scripts/check-claims.php | 5 +- docs/scripts/check-integrity.mjs | 8 +- docs/scripts/rules-snapshot.json | 6 +- docs/src/adapters/phpstan.md | 2 +- docs/src/adapters/phpunit.md | 11 +- docs/src/adapters/psalm.md | 11 + docs/src/adapters/testo.md | 10 +- docs/src/api/classes/Arg.md | 4 +- docs/src/api/classes/Captor.md | 2 +- docs/src/api/classes/Cardinality.md | 76 -- .../Exception/AmbiguousDefaultFactory.md | 2 +- .../classes/Exception/BypassUnavailable.md | 5 +- docs/src/api/classes/Exception/CannotWire.md | 2 +- .../Exception/ConflictingExpectation.md | 2 +- .../Exception/ContextOwnershipViolation.md | 5 +- .../api/classes/Exception/ForgottenDouble.md | 5 +- .../Exception/ForwardingTargetMismatch.md | 2 +- .../Exception/InvalidCallSpecification.md | 65 +- .../classes/Exception/InvalidDefaultValue.md | 2 +- .../Exception/InvalidSpecificationArgument.md | 57 +- .../api/classes/Exception/MatcherLeaked.md | 2 +- .../classes/Exception/NeverMethodCalled.md | 2 +- .../api/classes/Exception/NoDefaultValue.md | 2 +- .../api/classes/Exception/NothingCaptured.md | 2 +- .../Exception/OriginalCallUnavailable.md | 2 +- .../Exception/OriginalReturnTypeViolation.md | 2 +- .../classes/Exception/OutcomeUnavailable.md | 2 +- .../classes/Exception/StrictModeViolation.md | 2 +- .../api/classes/Exception/UnderstudyError.md | 2 +- .../classes/Exception/UnsupportedTarget.md | 5 +- .../classes/Exception/VerificationFailed.md | 2 +- docs/src/api/classes/ExpectBuilder.md | 2 +- docs/src/api/classes/FailureKind.md | 2 +- docs/src/api/classes/Invocation.md | 20 +- docs/src/api/classes/Outcome.md | 73 -- .../PhpUnit/UnderstudyPHPUnitIntegration.md | 28 +- .../classes/Testo/UnderstudyInterceptor.md | 2 +- .../src/api/classes/Testo/UnderstudyPlugin.md | 2 +- docs/src/api/classes/Understudy.md | 11 +- docs/src/api/classes/VerificationFailure.md | 11 +- docs/src/api/classes/WhenBuilder.md | 16 +- docs/src/api/exceptions.md | 4 +- docs/src/api/functions.md | 4 +- docs/src/api/index.md | 6 +- docs/src/api/rules.md | 12 +- docs/src/guide/doubles/creating.md | 2 +- docs/src/guide/failure-messages.md | 16 +- docs/src/guide/lifecycle/index.md | 6 +- docs/src/guide/modes.md | 8 + docs/src/guide/security.md | 9 + docs/src/guide/static-analysis.md | 5 +- docs/src/guide/stubbing/matchers.md | 16 +- infection.json5 | 10 + llms.txt | 29 +- .../skills/rasuvaeff-understudy/SKILL.md | 33 +- src/Arg.php | 8 +- src/Cardinality.php | 20 +- src/Exception/BypassUnavailable.php | 4 + src/Exception/ContextOwnershipViolation.php | 4 + src/Exception/ForgottenDouble.php | 4 + src/Exception/InvalidCallSpecification.php | 71 +- .../InvalidSpecificationArgument.php | 57 +- src/Exception/UnsupportedTarget.php | 4 + src/Invocation.php | 25 + src/Outcome.php | 8 +- src/Runtime/Runtime.php | 7 +- src/Understudy.php | 24 +- src/VerificationFailure.php | 9 +- src/WhenBuilder.php | 15 +- src/functions.php | 1 + tests/ArgTest.php | 14 +- tests/Defaults/TypeDefaultResolverTest.php | 4 +- tests/ErrorPathTest.php | 4 +- tests/LastCallAndForgetTest.php | 33 + 82 files changed, 1200 insertions(+), 801 deletions(-) delete mode 100644 docs/src/api/classes/Cardinality.md delete mode 100644 docs/src/api/classes/Outcome.md diff --git a/.gitattributes b/.gitattributes index f6adc7d..edcf5aa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,7 @@ /.gitignore export-ignore /.github export-ignore /.php-cs-fixer.php export-ignore +/.vale.ini export-ignore /AGENTS.md export-ignore /CLAUDE.md export-ignore /examples export-ignore diff --git a/AGENTS.md b/AGENTS.md index 60c87e8..dc42c82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,10 +11,11 @@ service methods on the double itself. Public API lives in `Rasuvaeff\Understudy`: the `Understudy` facade, the free functions `when()`/`expect()`/`expectSequence()`/`verify()` in -`src/functions.php`, `Arg`, `Invocation`, `Outcome`, `WhenBuilder`, and the -exceptions under `Exception\`. Everything -under `Codegen\`, `Runtime\`, `Expectation\`, `Defaults\` and `Matcher\` is -`@internal`. +`src/functions.php`, `Arg`, `Captor`, `Invocation`, `WhenBuilder`/`ExpectBuilder`, +`FailureKind`, `VerificationFailure`, and the exceptions under `Exception\`. +`Outcome` and `Cardinality` are `@internal` since 0.9 — no public path accepts +or returns either — like everything under `Codegen\`, `Runtime\`, +`Expectation\`, `Defaults\` and `Matcher\`. The design plan that drove milestones 0-7 shipped in full and was retired from the monorepo on 2026-08-28; what it decided lives in this file, the diff --git a/CHANGELOG.md b/CHANGELOG.md index 70677d8..b2080f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,80 @@ # Changelog +## 0.9.0 — 2026-09-05 + +The 1.0 candidate. A minor rather than a patch, and the last one before the +contract freezes: every decision of the 1.0-readiness review lands here, so +that the tag after this one can be `1.0.0` without a change to the surface. +Two of them are breaking on 0.x and Composer's caret already treats them so. + +- **`forget()` answers `ForgottenDouble` for a double that is gone.** Called a + second time on the same double, or on one a `reset()` already dropped, it + said «Understudy::forget() expects an understudy created by Understudy::for(). + This object is not one» — an `InvalidCallSpecification` about an object that + was one. It now goes through the same door as every other facade, so the + answer is `ForgottenDouble` (retired, or gone with a reset), a stranger is + still refused by name, and a foreign context is still + `ContextOwnershipViolation`. +- **One rule for the two specification exceptions, and two factories move.** + `InvalidCallSpecification` is about the SHAPE of a specification — what is + called, where, how many times — and `InvalidSpecificationArgument` about a + VALUE inside it that no run could act on. An inverted `Arg::int/float/count()` + range and an `Arg::string()` pattern PCRE cannot compile are values, so + `invertedBounds()` and `invalidPattern()` move to `InvalidSpecificationArgument` + beside `Arg::instanceOf()`'s unloadable type, `times(5, 2)` and `returns()`. + A `catch (InvalidCallSpecification)` around those two paths has to become + `catch (InvalidSpecificationArgument)` — or `catch (UnderstudyError)`, which + both implement. Both classes say the rule in their docblocks. +- **`Outcome` and `Cardinality` are `@internal`.** No public path accepted or + returned either: dispatch records outcomes through `Invocation`'s scalar + recorders and `Invocation::$outcome` stayed `null` for every call the engine + made, while `times()` and `verify()` take integers and a `VerificationFailure` + carries bounds as integers. Both were `@api` by inheritance from the first + design. `Invocation::recordOutcome()` and `Invocation::__construct()` are + `@internal` too — the dispatcher's bookkeeping, not a way to build a call by + hand. +- **`WhenBuilder` is closed by contract.** Its docblock now says that + subclassing is not supported and that the `protected` expectation it carries + is an `@internal` type; the keyword stays off only because `ExpectBuilder` is + the subclass, and Psalm refuses a `@final` tag for the same reason. +- **How `FailureKind` grows is written down.** The readonly fields of + `VerificationFailure` and every existing case are stable; a NEW case may + arrive in a minor, so a `match` over the enum needs a `default` arm. The + docblock, both READMEs, `llms.txt`, the skill file and the site say the same + thing — the 0.2.0 entry implied the opposite. +- Docblocks that the API reference renders: `checkpoint()` said «the current + context» and spans every context the test used; `verify()` as a free + function did not document `$never`; `Arg::bool()`, `WhenBuilder::throws()`, + `Invocation::didReturn()/didThrow()/returned()/thrown()` and seven exception + factories rendered as bare signatures. +- **The analyser parity matrix gains five idioms** (`bin/consumer-smoke`): + cardinality written past the first link, named bounds in either order, a + double reached through a helper, a first-class callable, and `returns()` on a + void method past the first link. The 0.8.0 wave fixed the first four in both + plugins without a row for any of them; the fifth is the same blindness found + a week later in `understudy-phpstan` 0.5.1. +- **The skill file caught up with the engine.** `resources/skills/…/SKILL.md` + — the one document that ships in the archive — said a nullable return answers + `null` before the registry is consulted (the opposite has been true since + 0.1.0), counted three free functions, listed no `allOf`/`anyOf`/`rest`/ + `captor` and mentioned neither `expectSequence()` nor `lean()`. +- Documentation catch-ups: seven 0.8.0 corrections reached the READMEs and + never the site (property defaults on class doubles, built-in interfaces as + return types, what `checkpoint()` clears, verbatim arguments and + `#[\SensitiveParameter]`, `instanceOf()` and PCRE `$` on the matchers page); + the Psalm page did not say that a leaked matcher is reported only at + `errorLevel="1"`; the adapter pages missed the 0.3.0/0.2.0 changes; the + Mockery table mapped `shouldNotHaveReceived()` to `unused()`, which asserts + no call at all; the README still told readers to expect a «too few + arguments» report on `Arg::rest()` that both analyser packages have handled + since 0.2.0. +- Rector is green again — red since v0.7.0, under four releases with green + builds. `infection/infection` `^0.35`, `rasuvaeff/property-testing-testo` + `^0.9` (it was three minors behind), `.vale.ini` is `export-ignore`. The + mutation gate stays at 92 with the reason beside the number: the run + measures 94.1%, three mutants above 94, which is less than the CI count + moves between runs. + ## 0.8.0 — 2026-09-05 - **`Understudy::for()` no longer kills the process on five built-in diff --git a/README.md b/README.md index 104be05..34e215a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ here. Two rows are traps, marked ⚠. | `->andThrow(new NotFound())` | `->throws(new NotFound())` | | | `->with(123, Mockery::any())` | inside the closure: `find(123, Arg::any())` | | | `Mockery::on(fn ($x) => ...)` | `Arg::satisfies(fn ($x) => ...)` | | -| `$mock->shouldNotHaveReceived('save')` | `Understudy::unused($mock)` | | +| `$mock->shouldNotHaveReceived('save')` | `verify(fn () => $mock->save(...), never: true)` | `Understudy::unused($mock)` when the double must not have been touched at all | | `$mock->shouldHaveReceived('save')` | `verify(fn () => $mock->save(...))` | after the fact; add `nothingElse()` — see below | | `Mockery::close()` | adapter's `reset()`, or your own teardown | | | `->makePartial()` / `Mockery::spy($real)` | `Understudy::delegate(Contract::class, $real)` + stubs on top | a stub wins, everything else runs for real — and is recorded | @@ -306,16 +306,18 @@ when(fn () => $storage->recordOutcome('svc', Arg::rest())) A specification that stops early *without* ending in `Arg::rest()` is refused with the reason, rather than becoming a stub that silently never matches. A later, narrower specification for the same call still wins over the broad -prefix stub. One thing to know: a static analyser reads the shortened call -against the contract's arity, so expect a "too few arguments" diagnostic on -that line until your analyser knows the idiom. +prefix stub. A static analyser reads the shortened call against the contract's +arity; the [understudy-psalm](https://github.com/rasuvaeff/understudy-psalm) +plugin and the [understudy-phpstan](https://github.com/rasuvaeff/understudy-phpstan) +extension know the idiom and keep that line quiet. The type matchers are deliberately strict: `Arg::int()` rejects `'5'`, and `Arg::float()` rejects `1`. A matcher pins the declared type as much as the value, which is the point in a codebase that runs with `strict_types`. -A matcher that could never match anything is refused where it is written, not -left to fail an expectation in teardown: `Arg::int(min: 5, max: 1)` and its +A matcher that could never match anything is refused where it is written — +with `InvalidSpecificationArgument` — rather than left to fail an expectation +in teardown: `Arg::int(min: 5, max: 1)` and its `float`/`count` siblings describe an empty range, `Arg::string('/[unclosed')` is not a pattern PCRE compiles — the latter would also raise a warning inside the code under test on every call — and `Arg::instanceOf()` needs a class or @@ -821,10 +823,11 @@ it. A message is written for a reader, and its wording is not part of the package's public contract: a patch release may reword one. Anything that acts on a failure rather than printing it — a runner adapter, an IDE plugin, a report -aggregator — reads `VerificationFailed::failures()` instead, whose -`FailureKind` cases and `VerificationFailure` readonly fields are frozen API -from v0.1.0. A test asserting on the exact text of a message is asserting on -prose. +aggregator — reads `VerificationFailed::failures()` instead: the readonly +fields of `VerificationFailure` and every existing `FailureKind` case are +stable, and a **new** kind may arrive in a minor release, so match on the enum +with a `default` arm rather than exhaustively. A test asserting on the exact +text of a message is asserting on prose. ### Cleaning up diff --git a/README.ru.md b/README.ru.md index 2c49245..59b5e93 100644 --- a/README.ru.md +++ b/README.ru.md @@ -58,7 +58,7 @@ when(fn () => $repository->find(123))->returns($book); | `->andThrow(new NotFound())` | `->throws(new NotFound())` | | | `->with(123, Mockery::any())` | в замыкании: `find(123, Arg::any())` | | | `Mockery::on(fn ($x) => ...)` | `Arg::satisfies(fn ($x) => ...)` | | -| `$mock->shouldNotHaveReceived('save')` | `Understudy::unused($mock)` | | +| `$mock->shouldNotHaveReceived('save')` | `verify(fn () => $mock->save(...), never: true)` | `Understudy::unused($mock)` — когда дубль не должны были трогать вообще | | `$mock->shouldHaveReceived('save')` | `verify(fn () => $mock->save(...))` | пост-фактум; добавьте `nothingElse()` — см. ниже | | `Mockery::close()` | reset() адаптера или свой teardown | | | `->makePartial()` / `Mockery::spy($real)` | `Understudy::delegate(Contract::class, $real)` + стабы поверх | стаб побеждает, остальное выполняется по-настоящему — и записывается | @@ -304,16 +304,19 @@ when(fn () => $storage->recordOutcome('svc', Arg::rest())) Спецификация, остановившаяся раньше *без* `Arg::rest()` в конце, отвергается с объяснением — вместо того чтобы стать стабом, который молча никогда не совпадёт. Более поздняя узкая спецификация того же вызова по-прежнему -побеждает широкий префиксный стаб. Один нюанс: статанализатор читает -укороченный вызов против арности контракта и сообщит «too few arguments» на -этой строке, пока не знает идиому. +побеждает широкий префиксный стаб. Статанализатор читает укороченный вызов +против арности контракта; плагин +[understudy-psalm](https://github.com/rasuvaeff/understudy-psalm) и расширение +[understudy-phpstan](https://github.com/rasuvaeff/understudy-phpstan) знают +идиому и эту строку не краснят. Типовые матчеры намеренно строгие: `Arg::int()` отвергает `'5'`, а `Arg::float()` отвергает `1`. Матчер закрепляет объявленный тип не меньше, чем значение, — ради этого он и нужен в коде со `strict_types`. -Матчер, который не смог бы совпасть ни с чем, отвергается там, где написан, а -не проваливает ожидание в teardown: `Arg::int(min: 5, max: 1)` и его собратья +Матчер, который не смог бы совпасть ни с чем, отвергается там, где написан — +исключением `InvalidSpecificationArgument`, — а не проваливает ожидание в +teardown: `Arg::int(min: 5, max: 1)` и его собратья `float`/`count` описывают пустой диапазон, `Arg::string('/[unclosed')` — не компилируемый PCRE-паттерн, который вдобавок поднимал бы warning внутри тестируемого кода на каждом вызове, а `Arg::instanceOf()` требует загружаемый @@ -820,9 +823,10 @@ Configure it first: when(fn () => $double->tag(...))->returns(...) Сообщение написано для человека, и его формулировка в публичный контракт пакета не входит: патч-релиз вправе переписать текст. Всё, что действует по ошибке, а не печатает её — адаптер раннера, плагин IDE, агрегатор отчётов, — -читает `VerificationFailed::failures()`: кейсы `FailureKind` и readonly-поля -`VerificationFailure` заморожены как публичный API с v0.1.0. Тест, который -ассертит точный текст сообщения, ассертит прозу. +читает `VerificationFailed::failures()`: readonly-поля `VerificationFailure` и +каждый существующий кейс `FailureKind` стабильны, а **новый** кейс может +приехать в миноре — поэтому `match` по enum пишите с ветвью `default`, а не +исчерпывающим. Тест, который ассертит точный текст сообщения, ассертит прозу. ### Очистка diff --git a/bin/consumer-smoke b/bin/consumer-smoke index c8c0300..910c0b1 100755 --- a/bin/consumer-smoke +++ b/bin/consumer-smoke @@ -532,8 +532,16 @@ PHP # Both are shapes an analyser can only get right by resolving the class, and # both were got wrong by reading the source text instead — in Psalm, in two # separate places, each found only because the other analyser disagreed. -PARITY_SILENT='OwnUnqualified OwnAliased OwnQualified StaticVerb RestShort CaptorSpec' -PARITY_REPORTED='ForeignUnqualified ForeignAliased ForeignCapture RealCall RestLeak CaptorLeak' +# +# The second group is the misuse rules, added after the 2026-09-05 review found +# both plugins wrong on the same four shapes and the matrix silent about all +# of them: cardinality read only on the first link of the chain, named bounds +# read positionally, a helper call counted as a second specified call, and a +# first-class callable that crashed one analyser outright. `VoidReturnsChain` +# is the same blindness on `returns()` a week later. A shape the plugins have +# an opinion about gets a file here the day the opinion is formed. +PARITY_SILENT='OwnUnqualified OwnAliased OwnQualified StaticVerb RestShort CaptorSpec NamedBoundsSwapped HelperReceiver FirstClassCallable' +PARITY_REPORTED='ForeignUnqualified ForeignAliased ForeignCapture RealCall RestLeak CaptorLeak CardinalityAfterVerb VoidReturnsChain' # Writes the matrix into a leg's `src/`, beside whatever that leg already has. parity_fixtures() { # dir @@ -862,6 +870,130 @@ final class CaptorLeak return $gate->open($code->capture()); } } +PHP + + cat > "$src/Log.php" <<'PHP' + "$src/CardinalityAfterVerb.php" <<'PHP' + $gate->open(1))->returns(true)->times(5, 2); + } +} +PHP + + cat > "$src/NamedBoundsSwapped.php" <<'PHP' + $gate->open(1))->times(maximum: 5, minimum: 1); + } +} +PHP + + cat > "$src/HelperReceiver.php" <<'PHP' + $this->gate()->open(1)); + } + + private function gate(): Gate + { + return Understudy::for(Gate::class); + } +} +PHP + + cat > "$src/FirstClassCallable.php" <<'PHP' + "$src/VoidReturnsChain.php" <<'PHP' +write('x'); + })->times(2)->returns('done'); + } +} PHP } diff --git a/composer.json b/composer.json index 42f35e5..0e79302 100644 --- a/composer.json +++ b/composer.json @@ -30,9 +30,9 @@ "require-dev": { "ergebnis/composer-normalize": "^2.51", "friendsofphp/php-cs-fixer": "^3.95", - "infection/infection": "^0.33", + "infection/infection": "^0.35", "maglnet/composer-require-checker": "^4.17", - "rasuvaeff/property-testing-testo": "^0.6", + "rasuvaeff/property-testing-testo": "^0.9", "rasuvaeff/rector-named-literals": "^1.0", "rector/rector": "^2.4", "roave/backward-compatibility-check": "^8.0", diff --git a/docs/scripts/api-snapshot.json b/docs/scripts/api-snapshot.json index c9b6b77..f8ae004 100644 --- a/docs/scripts/api-snapshot.json +++ b/docs/scripts/api-snapshot.json @@ -2,7 +2,7 @@ "classes": [ { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Arg", "kind": "class", @@ -33,7 +33,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 55 + "startLine": 56 }, { "name": "int", @@ -63,7 +63,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 64 + "startLine": 65 }, { "name": "float", @@ -93,7 +93,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 74 + "startLine": 75 }, { "name": "string", @@ -116,14 +116,14 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 86 + "startLine": 87 }, { "name": "bool", "static": true, "params": [], "returnType": "mixed", - "summary": "", + "summary": "Matches a `bool`, either value.", "description": "", "throws": [], "throwsInBody": false, @@ -131,7 +131,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 95 + "startLine": 99 }, { "name": "same", @@ -154,7 +154,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 103 + "startLine": 107 }, { "name": "not", @@ -177,7 +177,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 111 + "startLine": 115 }, { "name": "allOf", @@ -200,7 +200,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 122 + "startLine": 126 }, { "name": "anyOf", @@ -223,7 +223,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 133 + "startLine": 137 }, { "name": "instanceOf", @@ -246,7 +246,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 141 + "startLine": 145 }, { "name": "captor", @@ -269,7 +269,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 173 + "startLine": 177 }, { "name": "satisfies", @@ -299,7 +299,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 192 + "startLine": 196 }, { "name": "containing", @@ -322,7 +322,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 203 + "startLine": 207 }, { "name": "count", @@ -352,7 +352,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 214 + "startLine": 218 }, { "name": "which", @@ -382,7 +382,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 226 + "startLine": 230 }, { "name": "none", @@ -397,7 +397,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 330 + "startLine": 334 }, { "name": "remaining", @@ -412,7 +412,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 339 + "startLine": 343 }, { "name": "rest", @@ -427,17 +427,17 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 360 + "startLine": 364 } ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Arg.php#L48", + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Arg.php#L49", "implementedBy": [] }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Bypass\\FileWrapper", "kind": "class", @@ -1099,7 +1099,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Bypass\\FinalStripper", "kind": "class", @@ -1159,7 +1159,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Captor", "kind": "class", @@ -1287,18 +1287,22 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Cardinality", "kind": "class", - "isApi": true, + "isApi": false, "isAbstract": false, "isThrowable": false, "summary": "How many times a call is allowed to happen. `null` as the maximum means no\nupper bound.", - "description": "", + "description": "`@internal` since 0.9: no public path accepts or returns one — `times()` and\n`verify()` take integers, and a `VerificationFailure` carries the bounds as\n`expectedMinimum`/`expectedMaximum`. Declaring it `@api` promised a value\nobject nobody could hand to the library or get back from it.", "deprecated": null, "see": [], - "extensionTags": [], + "extensionTags": { + "internal": [ + "" + ] + }, "extends": null, "implements": [], "attributes": [], @@ -1337,7 +1341,7 @@ } ], "returnType": "Rasuvaeff\\Understudy\\Cardinality", - "summary": "", + "summary": "Exactly this many calls — the default an `expect()` starts with.", "description": "", "throws": [], "throwsInBody": false, @@ -1345,7 +1349,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 30 + "startLine": 38 }, { "name": "atLeast", @@ -1360,7 +1364,7 @@ } ], "returnType": "Rasuvaeff\\Understudy\\Cardinality", - "summary": "", + "summary": "This many calls or more, with no upper bound.", "description": "", "throws": [], "throwsInBody": false, @@ -1368,7 +1372,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 37 + "startLine": 48 }, { "name": "between", @@ -1390,7 +1394,7 @@ } ], "returnType": "Rasuvaeff\\Understudy\\Cardinality", - "summary": "", + "summary": "A closed range, or an open one when the maximum is null — the shape\n`times($minimum, $maximum)` and `verify(minimum:, maximum:)` build.", "description": "", "throws": [], "throwsInBody": false, @@ -1398,7 +1402,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 42 + "startLine": 57 }, { "name": "any", @@ -1413,14 +1417,14 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 68 + "startLine": 83 }, { "name": "never", "static": true, "params": [], "returnType": "Rasuvaeff\\Understudy\\Cardinality", - "summary": "", + "summary": "Not even once — `verify(…, never: true)`.", "description": "", "throws": [], "throwsInBody": false, @@ -1428,7 +1432,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 73 + "startLine": 91 }, { "name": "allows", @@ -1451,7 +1455,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 81 + "startLine": 99 }, { "name": "describe", @@ -1466,17 +1470,17 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 90 + "startLine": 108 } ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Cardinality.php#L15", + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Cardinality.php#L20", "implementedBy": [] }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Codegen\\Blueprint", "kind": "class", @@ -1613,7 +1617,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Codegen\\DoubleFactory", "kind": "class", @@ -1742,7 +1746,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Codegen\\MethodSignature", "kind": "class", @@ -1871,7 +1875,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Codegen\\PropertyDefaults", "kind": "class", @@ -1924,7 +1928,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Codegen\\PropertySignature", "kind": "class", @@ -2014,7 +2018,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Codegen\\TargetUnifier", "kind": "class", @@ -2067,7 +2071,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Codegen\\TypeRenderer", "kind": "class", @@ -2187,7 +2191,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Defaults\\DefaultFactories", "kind": "class", @@ -2292,7 +2296,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Defaults\\TypeDefaultResolver", "kind": "class", @@ -2431,7 +2435,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\AmbiguousDefaultFactory", "kind": "class", @@ -2491,7 +2495,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\BypassUnavailable", "kind": "class", @@ -2579,7 +2583,7 @@ } ], "returnType": "Rasuvaeff\\Understudy\\Exception\\BypassUnavailable", - "summary": "", + "summary": "Something else already transforms PHP source on `file://`; replacing it\nwould silently disable whatever it does.", "description": "", "throws": [], "throwsInBody": false, @@ -2587,7 +2591,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 41 + "startLine": 45 } ], "constants": [], @@ -2597,7 +2601,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\CannotWire", "kind": "class", @@ -2842,7 +2846,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\ConflictingExpectation", "kind": "class", @@ -2962,7 +2966,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\ContextOwnershipViolation", "kind": "class", @@ -2989,7 +2993,7 @@ "static": true, "params": [], "returnType": "Rasuvaeff\\Understudy\\Exception\\ContextOwnershipViolation", - "summary": "", + "summary": "A double was configured or verified from a context other than the one\nthat created it — another Fiber, or outside the scope it was built in.", "description": "", "throws": [], "throwsInBody": false, @@ -2997,7 +3001,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 16 + "startLine": 20 } ], "constants": [], @@ -3007,7 +3011,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\ForgottenDouble", "kind": "class", @@ -3103,7 +3107,7 @@ "static": true, "params": [], "returnType": "Rasuvaeff\\Understudy\\Exception\\ForgottenDouble", - "summary": "", + "summary": "A facade — `strict()`, `verify()`, `nothingElse()`, `forget()` itself —\nwas asked about a double that `Understudy::forget()` retired.", "description": "", "throws": [], "throwsInBody": false, @@ -3111,7 +3115,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 56 + "startLine": 60 }, { "name": "fromDefaultFactory", @@ -3134,7 +3138,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 68 + "startLine": 72 } ], "constants": [], @@ -3144,7 +3148,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\ForwardingTargetMismatch", "kind": "class", @@ -3234,15 +3238,15 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\InvalidCallSpecification", "kind": "class", "isApi": true, "isAbstract": false, "isThrowable": true, - "summary": "The closure handed to when()/verify()/calls() did not describe one call the\nway a specification must: no direct call on an understudy, more than one, or\narguments that cannot form a valid specification — which includes a matcher\nconfigured so that it could never match anything.", - "description": "", + "summary": "A specification of the wrong SHAPE: the closure handed to when()/verify()/\ncalls() made no direct call on an understudy, made it on a static method or\nthrew first; a tail matcher was not the last argument; the specification\nstopped before the required parameters ran out without saying so with\n`Arg::rest()`; a combinator was given nothing to combine; `verify()` was\nhanded arguments that contradict each other; a facade was handed an object\nthat is not a double; a protocol was armed over one still running.", + "description": "The line between this class and \\Rasuvaeff\\Understudy\\Exception\\InvalidSpecificationArgument: this\none is about how a specification is written — what is called, where, how\nmany times — and that one about a VALUE inside it that no run could act on\n(an inverted range, a count below zero, a pattern PCRE cannot compile, a\ntype that is not loadable). A `LogicException`, because the test is\nmalformed rather than given a bad number.", "deprecated": null, "see": [], "extensionTags": [], @@ -3261,7 +3265,7 @@ "static": true, "params": [], "returnType": "Rasuvaeff\\Understudy\\Exception\\InvalidCallSpecification", - "summary": "", + "summary": "The closure ran to its end without any generated method signalling: it\ncalled nothing on a double, or only something that is not one.", "description": "", "throws": [], "throwsInBody": false, @@ -3269,7 +3273,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 17 + "startLine": 31 }, { "name": "notADouble", @@ -3292,7 +3296,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 32 + "startLine": 46 }, { "name": "neverBesideACount", @@ -3315,14 +3319,14 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 48 + "startLine": 62 }, { "name": "exactCountBesideABound", "static": true, "params": [], "returnType": "Rasuvaeff\\Understudy\\Exception\\InvalidCallSpecification", - "summary": "", + "summary": "`verify($call, times: 2, minimum: 1)`: an exact count leaves a bound\nnothing to constrain, so one of the two is a mistake.", "description": "", "throws": [], "throwsInBody": false, @@ -3330,7 +3334,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 57 + "startLine": 75 }, { "name": "misplacedTailMatcher", @@ -3367,7 +3371,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 69 + "startLine": 87 }, { "name": "emptyCombinator", @@ -3390,75 +3394,8 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 84 - }, - { - "name": "invertedBounds", - "static": true, - "params": [ - { - "name": "matcher", - "type": "non-empty-string", - "description": "the factory that was called, without `Arg::`", - "default": null, - "variadic": false - }, - { - "name": "minimum", - "type": "int|float", - "description": "the lower bound as it was given", - "default": null, - "variadic": false - }, - { - "name": "maximum", - "type": "int|float", - "description": "the upper bound as it was given", - "default": null, - "variadic": false - } - ], - "returnType": "Rasuvaeff\\Understudy\\Exception\\InvalidCallSpecification", - "summary": "A range matcher whose maximum sits below its minimum, so it describes an\nempty range and could never match.", - "description": "", - "throws": [], - "throwsInBody": false, - "inheritedFrom": null, - "see": [], - "deprecated": null, - "attributes": [], "startLine": 102 }, - { - "name": "invalidPattern", - "static": true, - "params": [ - { - "name": "pattern", - "type": "non-empty-string", - "description": "the pattern as it was written", - "default": null, - "variadic": false - }, - { - "name": "reason", - "type": "non-empty-string|null", - "description": "what PCRE said, when it said anything", - "default": null, - "variadic": false - } - ], - "returnType": "Rasuvaeff\\Understudy\\Exception\\InvalidCallSpecification", - "summary": "A pattern handed to `Arg::string()` that PCRE cannot compile.", - "description": "", - "throws": [], - "throwsInBody": false, - "inheritedFrom": null, - "see": [], - "deprecated": null, - "attributes": [], - "startLine": 120 - }, { "name": "tailMatcherInCombinator", "static": true, @@ -3487,14 +3424,14 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 135 + "startLine": 116 }, { "name": "emptySequence", "static": true, "params": [], "returnType": "Rasuvaeff\\Understudy\\Exception\\InvalidCallSpecification", - "summary": "", + "summary": "`expectSequence()` with no steps: arming an empty protocol would put every\nlater call on trial with nothing to try it against.", "description": "", "throws": [], "throwsInBody": false, @@ -3502,7 +3439,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 146 + "startLine": 131 }, { "name": "protocolAlreadyArmed", @@ -3532,7 +3469,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 158 + "startLine": 143 }, { "name": "incompleteSpecification", @@ -3569,7 +3506,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 174 + "startLine": 159 }, { "name": "omittedBeforeSpecified", @@ -3606,7 +3543,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 190 + "startLine": 175 }, { "name": "omittedTailNeedsRest", @@ -3636,7 +3573,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 205 + "startLine": 190 }, { "name": "closureFailed", @@ -3651,7 +3588,7 @@ } ], "returnType": "Rasuvaeff\\Understudy\\Exception\\InvalidCallSpecification", - "summary": "", + "summary": "The specification closure threw before any generated method signalled;\nthe original is kept as `previous`, because it is the actual mistake.", "description": "", "throws": [], "throwsInBody": false, @@ -3659,7 +3596,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 216 + "startLine": 205 }, { "name": "staticMethodCalled", @@ -3682,17 +3619,17 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 228 + "startLine": 217 } ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidCallSpecification.php#L15", + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidCallSpecification.php#L25", "implementedBy": [] }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\InvalidDefaultValue", "kind": "class", @@ -3752,15 +3689,15 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\InvalidSpecificationArgument", "kind": "class", "isApi": true, "isAbstract": false, "isThrowable": true, - "summary": "An argument to a specification that no run could act on: a maximum call\ncount below its minimum, a negative count, `returns()` with nothing to\nreturn.", - "description": "Extends `\\InvalidArgumentException` because that is what these three paths\nhave always thrown, and a user's `catch (\\InvalidArgumentException $e)`\naround them must keep working. It implements `UnderstudyError` because that\ninterface says it is implemented by every exception this library throws,\nand these three were the exceptions to that — so a `catch (UnderstudyError\n$e)`, which the documentation recommends for catching misuse of Understudy\nitself, walked straight past them.", + "summary": "A VALUE inside a specification that no run could act on: a maximum call\ncount below its minimum, a negative count, `returns()` with nothing to\nreturn, a matcher range whose maximum sits below its minimum, a pattern\nPCRE cannot compile, `Arg::instanceOf()` naming a type that is not\nloadable.", + "description": "The line between this class and \\Rasuvaeff\\Understudy\\Exception\\InvalidCallSpecification: that one\nis about the SHAPE of a specification — what is called, where, how many\ntimes — and this one about a number, a pattern or a name given to it. It\nextends `\\InvalidArgumentException` because that is the SPL type these\npaths have always been, and a user's `catch (\\InvalidArgumentException $e)`\naround them keeps working; it implements `UnderstudyError` because that\ninterface is implemented by every exception this library throws.", "deprecated": null, "see": [], "extensionTags": [], @@ -3802,7 +3739,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 31 + "startLine": 33 }, { "name": "negativeCount", @@ -3825,7 +3762,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 45 + "startLine": 47 }, { "name": "noReturnValues", @@ -3840,7 +3777,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 54 + "startLine": 56 }, { "name": "unknownType", @@ -3863,17 +3800,84 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 66 + "startLine": 68 + }, + { + "name": "invertedBounds", + "static": true, + "params": [ + { + "name": "matcher", + "type": "non-empty-string", + "description": "the factory that was called, without `Arg::`", + "default": null, + "variadic": false + }, + { + "name": "minimum", + "type": "int|float", + "description": "the lower bound as it was given", + "default": null, + "variadic": false + }, + { + "name": "maximum", + "type": "int|float", + "description": "the upper bound as it was given", + "default": null, + "variadic": false + } + ], + "returnType": "Rasuvaeff\\Understudy\\Exception\\InvalidSpecificationArgument", + "summary": "A range matcher whose maximum sits below its minimum, so it describes an\nempty range and could never match.", + "description": "", + "throws": [], + "throwsInBody": false, + "inheritedFrom": null, + "see": [], + "deprecated": null, + "attributes": [], + "startLine": 84 + }, + { + "name": "invalidPattern", + "static": true, + "params": [ + { + "name": "pattern", + "type": "non-empty-string", + "description": "the pattern as it was written", + "default": null, + "variadic": false + }, + { + "name": "reason", + "type": "non-empty-string|null", + "description": "what PCRE said, when it said anything", + "default": null, + "variadic": false + } + ], + "returnType": "Rasuvaeff\\Understudy\\Exception\\InvalidSpecificationArgument", + "summary": "A pattern handed to `Arg::string()` that PCRE cannot compile.", + "description": "", + "throws": [], + "throwsInBody": false, + "inheritedFrom": null, + "see": [], + "deprecated": null, + "attributes": [], + "startLine": 102 } ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidSpecificationArgument.php#L22", + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidSpecificationArgument.php#L24", "implementedBy": [] }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\MatcherLeaked", "kind": "class", @@ -3940,7 +3944,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\NeverMethodCalled", "kind": "class", @@ -4060,7 +4064,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\NoDefaultValue", "kind": "class", @@ -4127,7 +4131,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\NothingCaptured", "kind": "class", @@ -4180,7 +4184,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\OriginalCallUnavailable", "kind": "class", @@ -4263,7 +4267,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\OriginalReturnTypeViolation", "kind": "class", @@ -4330,7 +4334,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\OutcomeUnavailable", "kind": "class", @@ -4413,7 +4417,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\StrictModeViolation", "kind": "class", @@ -4480,7 +4484,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\UnderstudyError", "kind": "interface", @@ -4529,7 +4533,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\UnsupportedTarget", "kind": "class", @@ -4564,7 +4568,7 @@ } ], "returnType": "Rasuvaeff\\Understudy\\Exception\\UnsupportedTarget", - "summary": "", + "summary": "No class or interface of that name is loadable — usually a typo, or a\nclass the autoloader cannot see from the test.", "description": "", "throws": [], "throwsInBody": false, @@ -4572,7 +4576,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 15 + "startLine": 19 }, { "name": "notDoublable", @@ -4602,7 +4606,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 26 + "startLine": 30 }, { "name": "signatureConflict", @@ -4639,7 +4643,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 38 + "startLine": 42 } ], "constants": [], @@ -4649,7 +4653,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Exception\\VerificationFailed", "kind": "class", @@ -4736,7 +4740,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\ExpectBuilder", "kind": "class", @@ -4784,7 +4788,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Expectation\\Action", "kind": "interface", @@ -4841,7 +4845,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Expectation\\ArgumentFormatter", "kind": "class", @@ -4924,7 +4928,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Expectation\\ComputeAnswer", "kind": "class", @@ -4991,7 +4995,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Expectation\\Expectation", "kind": "class", @@ -5449,7 +5453,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Expectation\\ReturnValue", "kind": "class", @@ -5516,7 +5520,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Expectation\\ThrowError", "kind": "class", @@ -5583,7 +5587,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\FailureKind", "kind": "enum", @@ -5672,7 +5676,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\FailureReport", "kind": "class", @@ -5857,7 +5861,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Invocation", "kind": "class", @@ -5943,7 +5947,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 68 + "startLine": 70 }, { "name": "recordFinalArguments", @@ -5966,7 +5970,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 78 + "startLine": 80 }, { "name": "belongsTo", @@ -5989,7 +5993,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 86 + "startLine": 88 }, { "name": "callOriginal", @@ -6009,7 +6013,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 102 + "startLine": 104 }, { "name": "recordOutcome", @@ -6024,7 +6028,7 @@ } ], "returnType": "void", - "summary": "", + "summary": "The wrapped form of recordReturned() and recordThrown(),\nkept for the callers that still hand over an `Outcome`; dispatch itself\nuses the scalar recorders because every call reaches them.", "description": "", "throws": [], "throwsInBody": false, @@ -6032,7 +6036,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 116 + "startLine": 125 }, { "name": "recordReturned", @@ -6055,7 +6059,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 128 + "startLine": 137 }, { "name": "recordDiscardedReturn", @@ -6070,7 +6074,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 146 + "startLine": 155 }, { "name": "isReturnDiscarded", @@ -6085,7 +6089,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 159 + "startLine": 168 }, { "name": "recordThrown", @@ -6108,7 +6112,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 167 + "startLine": 176 }, { "name": "markAccounted", @@ -6123,7 +6127,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 183 + "startLine": 192 }, { "name": "isAccounted", @@ -6138,14 +6142,14 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 191 + "startLine": 200 }, { "name": "didReturn", "static": false, "params": [], "returnType": "bool", - "summary": "", + "summary": "Whether the call answered with a value — `null` included, which is why\nthis is asked rather than inferred from returned().", "description": "", "throws": [], "throwsInBody": false, @@ -6153,14 +6157,14 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 196 + "startLine": 209 }, { "name": "didThrow", "static": false, "params": [], "returnType": "bool", - "summary": "", + "summary": "Whether the call ended in a throwable, which thrown() then holds.", "description": "", "throws": [], "throwsInBody": false, @@ -6168,29 +6172,34 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 201 + "startLine": 217 }, { "name": "returned", "static": false, "params": [], "returnType": "mixed", - "summary": "", + "summary": "The value the call answered with.", "description": "", - "throws": [], + "throws": [ + { + "type": "\\Rasuvaeff\\Understudy\\Exception\\OutcomeUnavailable", + "description": "when the call threw instead, or when the\nunderstudy is lean and did not keep the value" + } + ], "throwsInBody": true, "inheritedFrom": null, "see": [], "deprecated": null, "attributes": [], - "startLine": 208 + "startLine": 230 }, { "name": "thrown", "static": false, "params": [], "returnType": "?Throwable", - "summary": "", + "summary": "The throwable the call ended in, or null when it returned.", "description": "", "throws": [], "throwsInBody": false, @@ -6198,7 +6207,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 227 + "startLine": 252 } ], "constants": [], @@ -6208,7 +6217,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\AllOf", "kind": "class", @@ -6292,7 +6301,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\AnyArgument", "kind": "class", @@ -6366,7 +6375,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\AnyOf", "kind": "class", @@ -6450,7 +6459,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\AnyRest", "kind": "class", @@ -6550,7 +6559,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\AnyTail", "kind": "class", @@ -6650,7 +6659,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\ArgumentMatcher", "kind": "interface", @@ -6738,7 +6747,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\ArrayContaining", "kind": "class", @@ -6822,7 +6831,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\BooleanValue", "kind": "class", @@ -6896,7 +6905,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\Bounds", "kind": "class", @@ -6956,7 +6965,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\Capturing", "kind": "class", @@ -7049,7 +7058,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\CountBetween", "kind": "class", @@ -7142,7 +7151,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\EmptyTail", "kind": "class", @@ -7242,7 +7251,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\FloatInRange", "kind": "class", @@ -7335,7 +7344,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\IdenticalTo", "kind": "class", @@ -7419,7 +7428,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\InstanceOfType", "kind": "class", @@ -7503,7 +7512,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\IntInRange", "kind": "class", @@ -7596,7 +7605,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\Negated", "kind": "class", @@ -7680,7 +7689,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\Operand", "kind": "class", @@ -7763,7 +7772,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\QueryEquals", "kind": "class", @@ -7856,7 +7865,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\Satisfying", "kind": "class", @@ -7949,7 +7958,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\StringMatching", "kind": "class", @@ -8033,7 +8042,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Matcher\\TailMatcher", "kind": "interface", @@ -8092,18 +8101,22 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Outcome", "kind": "class", - "isApi": true, + "isApi": false, "isAbstract": false, "isThrowable": false, "summary": "How one call ended: with a value or with a throwable. Kept as its own type\nbecause `null` is a perfectly valid return value and cannot double as\n\"nothing was returned\".", - "description": "", + "description": "`@internal` since 0.9: dispatch records outcomes through the scalar path\n(`Invocation::recordReturned()` / `recordThrown()`), so no public path ever\nbuilds or receives one — `Invocation::didReturn()`, `returned()`, `didThrow()`\nand `thrown()` are the public reading of an outcome. It was `@api` by\ninheritance from the first design, not because anything reached it.", "deprecated": null, "see": [], - "extensionTags": [], + "extensionTags": { + "internal": [ + "" + ] + }, "extends": null, "implements": [], "attributes": [], @@ -8159,7 +8172,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 24 + "startLine": 30 }, { "name": "thrownError", @@ -8182,7 +8195,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 29 + "startLine": 35 }, { "name": "didReturn", @@ -8197,7 +8210,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 34 + "startLine": 40 }, { "name": "didThrow", @@ -8212,7 +8225,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 39 + "startLine": 45 }, { "name": "returned", @@ -8235,7 +8248,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 47 + "startLine": 53 }, { "name": "thrown", @@ -8250,18 +8263,18 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 58 + "startLine": 64 } ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Outcome.php#L16", + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Outcome.php#L22", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\ArgReturnType", "kind": "class", "isApi": false, @@ -8368,13 +8381,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/ArgReturnType.php#L40", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/ArgReturnType.php#L40", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\BuilderReturnType", "kind": "class", "isApi": false, @@ -8464,13 +8477,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/BuilderReturnType.php#L23", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/BuilderReturnType.php#L23", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\CaptorReturnType", "kind": "class", "isApi": false, @@ -8577,13 +8590,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/CaptorReturnType.php#L30", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/CaptorReturnType.php#L30", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Collector\\CaptureCallCollector", "kind": "class", "isApi": false, @@ -8661,13 +8674,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Collector/CaptureCallCollector.php#L27", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Collector/CaptureCallCollector.php#L27", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Collector\\MatcherCallCollector", "kind": "class", "isApi": false, @@ -8745,13 +8758,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Collector/MatcherCallCollector.php#L20", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Collector/MatcherCallCollector.php#L20", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Collector\\SpecificationRangeCollector", "kind": "class", "isApi": false, @@ -8829,13 +8842,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Collector/SpecificationRangeCollector.php#L27", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Collector/SpecificationRangeCollector.php#L27", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\BuilderType", "kind": "class", "isApi": false, @@ -8896,13 +8909,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/BuilderType.php#L30", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/BuilderType.php#L30", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\Cardinality", "kind": "class", "isApi": false, @@ -8979,13 +8992,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/Cardinality.php#L16", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/Cardinality.php#L16", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\ClosureShape", "kind": "class", "isApi": false, @@ -9066,13 +9079,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/ClosureShape.php#L29", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/ClosureShape.php#L29", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\MatcherKind", "kind": "class", "isApi": false, @@ -9126,13 +9139,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/MatcherKind.php#L28", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/MatcherKind.php#L28", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\MatcherName", "kind": "class", "isApi": false, @@ -9179,13 +9192,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/MatcherName.php#L22", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/MatcherName.php#L22", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\ResolvedName", "kind": "class", "isApi": false, @@ -9232,13 +9245,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/ResolvedName.php#L20", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/ResolvedName.php#L20", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\SpecificationCheck", "kind": "class", "isApi": false, @@ -9318,13 +9331,13 @@ } ], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/SpecificationCheck.php#L35", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/SpecificationCheck.php#L35", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\SpecificationExpr", "kind": "class", "isApi": false, @@ -9371,13 +9384,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/SpecificationExpr.php#L22", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/SpecificationExpr.php#L22", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\SpecifiedCall", "kind": "class", "isApi": false, @@ -9489,13 +9502,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/SpecifiedCall.php#L31", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/SpecifiedCall.php#L31", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\VerbNames", "kind": "class", "isApi": false, @@ -9602,13 +9615,13 @@ } ], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/VerbNames.php#L16", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/VerbNames.php#L16", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Internal\\WireShape", "kind": "class", "isApi": false, @@ -9672,13 +9685,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/WireShape.php#L30", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/WireShape.php#L30", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\RestArity", "kind": "class", "isApi": false, @@ -9743,13 +9756,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/RestArity.php#L34", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/RestArity.php#L34", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Rule\\FluentCardinalityRule", "kind": "class", "isApi": false, @@ -9827,13 +9840,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Rule/FluentCardinalityRule.php#L27", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Rule/FluentCardinalityRule.php#L27", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Rule\\MatcherLeakRule", "kind": "class", "isApi": false, @@ -9918,13 +9931,13 @@ } ], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Rule/MatcherLeakRule.php#L34", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Rule/MatcherLeakRule.php#L34", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Rule\\SpecificationCallRule", "kind": "class", "isApi": false, @@ -10002,13 +10015,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Rule/SpecificationCallRule.php#L21", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Rule/SpecificationCallRule.php#L21", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Rule\\SpecificationStaticCallRule", "kind": "class", "isApi": false, @@ -10086,13 +10099,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Rule/SpecificationStaticCallRule.php#L23", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Rule/SpecificationStaticCallRule.php#L23", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\Rule\\VoidReturnsRule", "kind": "class", "isApi": false, @@ -10177,13 +10190,13 @@ } ], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Rule/VoidReturnsRule.php#L34", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Rule/VoidReturnsRule.php#L34", "implementedBy": [] }, { "root": "phpstan", - "rootVersion": "v0.2.0", - "rootReference": "911d4c47950803ea3e4fed74112598c1db09d640", + "rootVersion": "v0.2.1", + "rootReference": "55df82300b4eba83c101d835f6abe02159215d97", "class": "Rasuvaeff\\Understudy\\PhpStan\\StaticBuilderReturnType", "kind": "class", "isApi": false, @@ -10300,20 +10313,20 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/StaticBuilderReturnType.php#L29", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/StaticBuilderReturnType.php#L29", "implementedBy": [] }, { "root": "phpunit", - "rootVersion": "v0.1.3", - "rootReference": "d8389a7e82bfc020c02cbe37122593f8f949d664", + "rootVersion": "v0.1.7", + "rootReference": "d48d08dcb2d23a0359d2f756cc51e96c8ece7624", "class": "Rasuvaeff\\Understudy\\PhpUnit\\UnderstudyPHPUnitIntegration", "kind": "trait", "isApi": true, "isAbstract": false, "isThrowable": false, "summary": "Ends every PHPUnit test with understudy's own bookkeeping done for it.", - "description": "```php\nfinal class CheckoutTest extends TestCase\n{\n use UnderstudyPHPUnitIntegration;\n\n public function testChargesForTheCart(): void\n {\n $books = Understudy::for(BookRepositoryInterface::class);\n when(fn () => $books->find(7))->returns(new Book(7));\n\n (new Checkout($books))->charge([7]);\n\n expect(fn () => $books->find(7)); // verified for you\n }\n}\n```\n\nOn a test that reached \\PHPUnit\\Framework\\TestCase::assertPostConditions() — that is,\npassed its body — the whole context is verified: an `expect()` the code\nnever fulfilled fails the test as an assertion failure. A test whose body\nthrew keeps its own exception untouched; verification would only mask the\nerror that actually happened. Either way the context is reset, so nothing\nleaks into the next test.\n\nA base class may flip strict stubbing for a whole project by overriding\n\\Rasuvaeff\\Understudy\\PhpUnit\\understudyStrictStubs().\n\nIf the class also overrides `assertPostConditions()` itself, PHP resolves\nthe conflict silently in favour of the class — the trait's verification\nwould stop running without any error. Compose explicitly instead:\n\n```php\nuse Rasuvaeff\\Understudy\\PhpUnit\\UnderstudyPHPUnitIntegration {\n UnderstudyPHPUnitIntegration::assertPostConditions\n as understudyAssertPostConditions;\n}\n\nprotected function assertPostConditions(): void\n{\n $this->understudyAssertPostConditions();\n // your post-conditions ...\n}\n```", + "description": "```php\nfinal class CheckoutTest extends TestCase\n{\n use UnderstudyPHPUnitIntegration;\n\n public function testChargesForTheCart(): void\n {\n $books = Understudy::for(BookRepositoryInterface::class);\n expect(fn () => $books->find(7))->returns($expected = new Book(7));\n\n $receipt = (new Checkout($books))->charge([7]);\n\n self::assertSame($expected->price, $receipt->total);\n }\n}\n```\n\nOne registration says both things: `find(7)` must be called exactly once,\nand it answers `$expected`. Two rules of the engine decide that shape, and\nthis snippet used to break both:\n\n- **Arm before the run.** An `expect()` counts only the calls that arrive\n after it is declared. Written below the action it counts zero and fails\n as \"called never\" about a call that did happen; to claim a call that has\n already happened, use `verify()`.\n- **One registration per call.** A `when()` stub and an `expect()` naming\n the same call are two registrations of one call, and the engine answers\n with `ConflictingExpectation`.\n\nKept in step with the README's Usage section and `examples/readme-usage.php`,\nwhich runs it; `DocumentedUsageTest` fails when the three drift apart.\n\nOn a test that reached \\PHPUnit\\Framework\\TestCase::assertPostConditions() — that is,\npassed its body — the whole context is verified: an `expect()` the code\nnever fulfilled fails the test as an assertion failure. A test whose body\nthrew keeps its own exception untouched; verification would only mask the\nerror that actually happened. Either way the context is reset, so nothing\nleaks into the next test.\n\nA base class may flip strict stubbing for a whole project by overriding\n\\Rasuvaeff\\Understudy\\PhpUnit\\understudyStrictStubs().\n\nIf the class also overrides `assertPostConditions()` itself, PHP resolves\nthe conflict silently in favour of the class — the trait's verification\nwould stop running without any error. Compose explicitly instead:\n\n```php\nuse Rasuvaeff\\Understudy\\PhpUnit\\UnderstudyPHPUnitIntegration {\n UnderstudyPHPUnitIntegration::assertPostConditions\n as understudyAssertPostConditions;\n}\n\nprotected function assertPostConditions(): void\n{\n // your post-conditions ...\n $this->understudyAssertPostConditions();\n}\n```\n\nThe user's post-conditions run first — the check closer to the test body\nwins, same as the README says.", "deprecated": null, "see": [], "extensionTags": { @@ -10329,7 +10342,7 @@ "publicMethods": [], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-phpunit/blob/d8389a7e82bfc020c02cbe37122593f8f949d664/src/PhpUnit/UnderstudyPHPUnitIntegration.php#L65", + "sourceUrl": "https://github.com/rasuvaeff/understudy-phpunit/blob/d48d08dcb2d23a0359d2f756cc51e96c8ece7624/src/PhpUnit/UnderstudyPHPUnitIntegration.php#L83", "implementedBy": [] }, { @@ -11456,7 +11469,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\Absent", "kind": "enum", @@ -11499,7 +11512,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\ArmedSequence", "kind": "class", @@ -11667,7 +11680,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\DoubleState", "kind": "class", @@ -12148,7 +12161,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\InvocationSignal", "kind": "class", @@ -12224,7 +12237,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\Mode", "kind": "enum", @@ -12287,7 +12300,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\ReferenceSlot", "kind": "class", @@ -12323,7 +12336,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\Runtime", "kind": "class", @@ -12358,7 +12371,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 88 + "startLine": 87 }, { "name": "currentIfAny", @@ -12373,7 +12386,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 102 + "startLine": 101 }, { "name": "pushScope", @@ -12388,7 +12401,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 113 + "startLine": 112 }, { "name": "popScope", @@ -12403,7 +12416,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 121 + "startLine": 120 }, { "name": "adopt", @@ -12433,7 +12446,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 187 + "startLine": 186 }, { "name": "liveContexts", @@ -12448,7 +12461,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 221 + "startLine": 220 }, { "name": "adoptClone", @@ -12471,7 +12484,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 252 + "startLine": 251 }, { "name": "adoptInto", @@ -12501,7 +12514,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 280 + "startLine": 279 }, { "name": "adoptContractsInto", @@ -12531,7 +12544,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 292 + "startLine": 291 }, { "name": "ownerOf", @@ -12554,7 +12567,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 308 + "startLine": 307 }, { "name": "isForgotten", @@ -12577,7 +12590,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 322 + "startLine": 321 }, { "name": "isRetiredOnPurpose", @@ -12600,7 +12613,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 330 + "startLine": 329 }, { "name": "forget", @@ -12618,12 +12631,12 @@ "summary": "Retires a double on purpose: its owner's state is dropped, so\nverification, `nothingElse()` and reset stop seeing it — a replacement\ndouble must not inherit its stubs into a `strictStubs` verdict. A call\non the object afterwards fails with `ForgottenDouble`, the same guard a\nscope close leaves behind.", "description": "", "throws": [], - "throwsInBody": true, + "throwsInBody": false, "inheritedFrom": null, "see": [], "deprecated": null, "attributes": [], - "startLine": 342 + "startLine": 341 }, { "name": "stateOf", @@ -12646,7 +12659,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 405 + "startLine": 404 }, { "name": "isOwnedByCurrentContext", @@ -12669,7 +12682,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 410 + "startLine": 409 }, { "name": "dispatch", @@ -12706,7 +12719,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 423 + "startLine": 422 }, { "name": "propertyRead", @@ -12736,7 +12749,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 736 + "startLine": 735 }, { "name": "propertyWrite", @@ -12773,7 +12786,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 772 + "startLine": 771 }, { "name": "referenceSlot", @@ -12810,7 +12823,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 830 + "startLine": 829 }, { "name": "callOriginal", @@ -12847,7 +12860,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 858 + "startLine": 857 }, { "name": "reset", @@ -12862,17 +12875,17 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 1018 + "startLine": 1017 } ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Runtime/Runtime.php#L29", + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/Runtime/Runtime.php#L28", "implementedBy": [] }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\RuntimeContext", "kind": "class", @@ -13219,7 +13232,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Runtime\\SequenceVerdict", "kind": "enum", @@ -13292,8 +13305,8 @@ }, { "root": "testo", - "rootVersion": "v0.1.3", - "rootReference": "2ff72ea1c4400953bf9437655ceb1df1d02ca350", + "rootVersion": "v0.1.4", + "rootReference": "dead543cd518323bf94aa1c7df69b947c71a40e9", "class": "Rasuvaeff\\Understudy\\Testo\\UnderstudyInterceptor", "kind": "class", "isApi": true, @@ -13360,13 +13373,13 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-testo/blob/2ff72ea1c4400953bf9437655ceb1df1d02ca350/src/Testo/UnderstudyInterceptor.php#L51", + "sourceUrl": "https://github.com/rasuvaeff/understudy-testo/blob/dead543cd518323bf94aa1c7df69b947c71a40e9/src/Testo/UnderstudyInterceptor.php#L51", "implementedBy": [] }, { "root": "testo", - "rootVersion": "v0.1.3", - "rootReference": "2ff72ea1c4400953bf9437655ceb1df1d02ca350", + "rootVersion": "v0.1.4", + "rootReference": "dead543cd518323bf94aa1c7df69b947c71a40e9", "class": "Rasuvaeff\\Understudy\\Testo\\UnderstudyPlugin", "kind": "class", "isApi": true, @@ -13423,12 +13436,12 @@ ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy-testo/blob/2ff72ea1c4400953bf9437655ceb1df1d02ca350/src/Testo/UnderstudyPlugin.php#L29", + "sourceUrl": "https://github.com/rasuvaeff/understudy-testo/blob/dead543cd518323bf94aa1c7df69b947c71a40e9/src/Testo/UnderstudyPlugin.php#L29", "implementedBy": [] }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Understudy", "kind": "class", @@ -13905,7 +13918,7 @@ "summary": "Retires an understudy on purpose.", "description": "For the double a test built and then replaced — `$this->generator =\n$this->fixedGenerator('other')` leaves the first one behind, still\nholding its stubs. Under `verifyAll(strictStubs: true)` that stub is a\nfailure about a double the test no longer uses; `forget()` says it was\nretired, so verification and reset stop seeing it. Calling anything on\nthe object afterwards fails with `ForgottenDouble`.\n\nOne-way, like every other form of forgetting here: a double belongs to\nexactly one context for its whole life.", "throws": [], - "throwsInBody": true, + "throwsInBody": false, "inheritedFrom": null, "see": [], "deprecated": null, @@ -13940,7 +13953,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 868 + "startLine": 867 }, { "name": "allVerified", @@ -13963,7 +13976,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 911 + "startLine": 910 }, { "name": "expectSequence", @@ -13986,7 +13999,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 996 + "startLine": 993 }, { "name": "verifySequence", @@ -14009,7 +14022,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 1036 + "startLine": 1033 }, { "name": "transcript", @@ -14032,7 +14045,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 1062 + "startLine": 1059 }, { "name": "scope", @@ -14062,7 +14075,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 1198 + "startLine": 1195 }, { "name": "checkpoint", @@ -14077,7 +14090,7 @@ } ], "returnType": "void", - "summary": "Verifies the current context and clears what has been settled, keeping\nthe understudies themselves — for a long test that runs in phases.", + "summary": "Verifies every context the test put understudies in and clears what has\nbeen settled — the claims a matching `expect()` or a successful\n`verify()` accounted for — keeping the understudies, their modes and\ntheir labels, for a long test that runs in phases. A call covered by a\n`when()` stub alone stays in the log, because `nothingElse()` still\nreads it; `reset()`, `scope()` and `lean()` are the ways to let go of\neverything.", "description": "", "throws": [], "throwsInBody": false, @@ -14085,7 +14098,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 1226 + "startLine": 1228 }, { "name": "reset", @@ -14100,7 +14113,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 1253 + "startLine": 1255 }, { "name": "idle", @@ -14115,7 +14128,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 1265 + "startLine": 1267 } ], "constants": [], @@ -14125,7 +14138,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\VerificationFailure", "kind": "class", @@ -14133,7 +14146,7 @@ "isAbstract": false, "isThrowable": false, "summary": "The structured half of one verification failure — the same facts the\nrendered message states, addressable by field.", - "description": "For tooling that wants to do something with a failure other than print it:\na runner adapter, an IDE plugin, a report aggregator. The rendered message\nstays the human surface; this class is the machine one.\n\nWhich fields are set depends on the kind:\n\n| Kind | `double` | `expectation` | `expectedMinimum`/`expectedMaximum` | `actualCount` | `observedCalls` | `expectedCalls` |\n|---|---|---|---|---|---|---|\n| `UnmetExpectation` | label | call spec | the claimed bounds | matched calls | calls to the same method | — |\n| `StrictStubUnused` | label | call spec | — | `0` | — | — |\n| `OutOfOrder` | label | the call that came first | — | — | — | — |\n| `OutOfSequence` | — | the mismatched position | — | calls made | the whole protocol | the specified sequence |\n| `UnaccountedCalls` | label | — | — | unaccounted calls | the unaccounted calls | — |\n| `UnusedDouble` | label | — | `0`/`0` | calls received | every call | — |\n\nThe readonly fields of this class, of \\Rasuvaeff\\Understudy\\FailureKind, and of the\nexceptions carrying them are frozen public API from v0.1.0: renaming,\nremoving or retyping any of them is a major-version change. New kinds and\nnewly-populated fields are additive and may arrive in a minor.", + "description": "For tooling that wants to do something with a failure other than print it:\na runner adapter, an IDE plugin, a report aggregator. The rendered message\nstays the human surface; this class is the machine one.\n\nWhich fields are set depends on the kind:\n\n| Kind | `double` | `expectation` | `expectedMinimum`/`expectedMaximum` | `actualCount` | `observedCalls` | `expectedCalls` |\n|---|---|---|---|---|---|---|\n| `UnmetExpectation` | label | call spec | the claimed bounds | matched calls | calls to the same method | — |\n| `StrictStubUnused` | label | call spec | — | `0` | — | — |\n| `OutOfOrder` | label | the call that came first | — | — | — | — |\n| `OutOfSequence` | — | the mismatched position | — | calls made | the whole protocol | the specified sequence |\n| `UnaccountedCalls` | label | — | — | unaccounted calls | the unaccounted calls | — |\n| `UnusedDouble` | label | — | `0`/`0` | calls received | every call | — |\n\nThe readonly fields of this class and every existing \\Rasuvaeff\\Understudy\\FailureKind\ncase are stable: renaming, removing or retyping one is a major-version\nchange. A NEW kind, or a field newly populated for an existing kind, may\narrive in a minor — so match on `FailureKind` with a `default` arm; an\nexhaustive `match` over the enum is not a supported way to consume it.", "deprecated": null, "see": [], "extensionTags": [], @@ -14227,12 +14240,12 @@ "publicMethods": [], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/VerificationFailure.php#L33", + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/VerificationFailure.php#L34", "implementedBy": [] }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\WhenBuilder", "kind": "class", @@ -14240,7 +14253,7 @@ "isAbstract": false, "isThrowable": false, "summary": "Configures what a stubbed call does.", - "description": "`TReturn` is the return type of the specified method. Plain Psalm and\nPHPStan cannot infer it from the closure on their own — `understudy-psalm`\nfills it in, and until then the parameter stays `mixed`, which is why the\ntemplate is declared here rather than added later: a published signature\ncannot grow one without changing its contract.\n\nNot `final`, and the one class here that is not: `ExpectBuilder` extends it\nto add the cardinality verbs, and the two are one fluent vocabulary rather\nthan two. The cost is that `protected readonly Expectation` — an\n`@internal` type — is reachable from a subclass a consumer could write. That\nis tolerated rather than intended: nothing in the contract invites it, and\nclosing it would mean duplicating every action verb.", + "description": "`TReturn` is the return type of the specified method. Plain Psalm and\nPHPStan cannot infer it from the closure on their own — `understudy-psalm`\nfills it in, and until then the parameter stays `mixed`, which is why the\ntemplate is declared here rather than added later: a published signature\ncannot grow one without changing its contract.\n\nNot `final`, and the one class here that is not: `ExpectBuilder` extends it\nto add the cardinality verbs, and the two are one fluent vocabulary rather\nthan two. For everyone else it is closed by contract, not by keyword:\nsubclassing is not supported, the `protected readonly Expectation` it\ncarries is an `@internal` type that may change in any release, and a subclass\nreading it is on its own. The keyword is missing only because closing the\nclass would mean duplicating every action verb on `ExpectBuilder` — and a\n`@final` tag is not an option either, since Psalm would then refuse\n`ExpectBuilder` itself.", "deprecated": null, "see": [], "extensionTags": { @@ -14285,7 +14298,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 53 + "startLine": 56 }, { "name": "throws", @@ -14300,7 +14313,7 @@ } ], "returnType": "static", - "summary": "", + "summary": "Throws this exact instance on the call — the same object every time the\nlink answers, which is what a test holding a reference to it expects.", "description": "", "throws": [], "throwsInBody": false, @@ -14308,7 +14321,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 75 + "startLine": 82 }, { "name": "answers", @@ -14331,7 +14344,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 85 + "startLine": 92 }, { "name": "then", @@ -14346,7 +14359,7 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 104 + "startLine": 111 }, { "name": "times", @@ -14376,17 +14389,17 @@ "see": [], "deprecated": null, "attributes": [], - "startLine": 121 + "startLine": 128 } ], "constants": [], "enumCases": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/WhenBuilder.php#L33", + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/WhenBuilder.php#L36", "implementedBy": [] }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "class": "Rasuvaeff\\Understudy\\Wiring\\Wire", "kind": "class", @@ -14451,7 +14464,7 @@ "functions": [ { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "kind": "function", "function": "Rasuvaeff\\Understudy\\expect", @@ -14475,7 +14488,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "kind": "function", "function": "Rasuvaeff\\Understudy\\expectSequence", @@ -14499,7 +14512,7 @@ }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "kind": "function", "function": "Rasuvaeff\\Understudy\\verify", @@ -14540,18 +14553,18 @@ { "name": "never", "type": "bool", - "description": "", + "description": "the call must not have happened at all; refused beside a count", "default": "false", "variadic": false } ], "returnType": "void", "throws": [], - "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/functions.php#L83" + "sourceUrl": "https://github.com/rasuvaeff/understudy/blob/master/src/functions.php#L84" }, { "root": "core", - "rootVersion": "v0.7.2", + "rootVersion": "v0.8.0", "rootReference": null, "kind": "function", "function": "Rasuvaeff\\Understudy\\when", diff --git a/docs/scripts/check-claims.php b/docs/scripts/check-claims.php index 5f68ad0..aa04d94 100644 --- a/docs/scripts/check-claims.php +++ b/docs/scripts/check-claims.php @@ -33,6 +33,7 @@ use Rasuvaeff\Understudy\Arg; use Rasuvaeff\Understudy\Exception\InvalidCallSpecification; +use Rasuvaeff\Understudy\Exception\InvalidSpecificationArgument; use Rasuvaeff\Understudy\Exception\VerificationFailed; use Rasuvaeff\Understudy\Invocation; use Rasuvaeff\Understudy\Understudy; @@ -349,7 +350,7 @@ final class Thrower { public function value(): string { throw new RuntimeExcepti claim('stubbing/matchers', 'an inverted range is refused where it is written', function (): bool|string { try { Arg::int(min: 5, max: 1); - } catch (InvalidCallSpecification) { + } catch (InvalidSpecificationArgument) { return true; } @@ -368,7 +369,7 @@ final class Thrower { public function value(): string { throw new RuntimeExcepti Arg::string('/[unclosed'); return 'Arg::string(\'/[unclosed\') was accepted'; - } catch (InvalidCallSpecification) { + } catch (InvalidSpecificationArgument) { return $raised === [] ?: 'the refusal raised the warning it exists to prevent'; } finally { restore_error_handler(); diff --git a/docs/scripts/check-integrity.mjs b/docs/scripts/check-integrity.mjs index a857b35..298764c 100644 --- a/docs/scripts/check-integrity.mjs +++ b/docs/scripts/check-integrity.mjs @@ -39,10 +39,10 @@ const pkgDir = join(docsDir, '..') // arrives documented or reddens the build. const COMPLETENESS_BUDGET = { 'type without a summary': 0, - 'method without a summary': 76, - 'parameter without a description': 162, - 'constructor parameter without a description': 15, - 'throwing method without @throws': 11, + 'method without a summary': 49, + 'parameter without a description': 154, + 'constructor parameter without a description': 10, + 'throwing method without @throws': 8, } const errors = [] diff --git a/docs/scripts/rules-snapshot.json b/docs/scripts/rules-snapshot.json index b57c92c..4fc2a77 100644 --- a/docs/scripts/rules-snapshot.json +++ b/docs/scripts/rules-snapshot.json @@ -1,8 +1,8 @@ { "phpstan": { - "version": "v0.2.0", - "reference": "911d4c47950803ea3e4fed74112598c1db09d640", - "repoBlob": "https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/", + "version": "v0.2.1", + "reference": "55df82300b4eba83c101d835f6abe02159215d97", + "repoBlob": "https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/", "registeredRules": [ "Rasuvaeff\\Understudy\\PhpStan\\Rule\\SpecificationCallRule", "Rasuvaeff\\Understudy\\PhpStan\\Rule\\SpecificationStaticCallRule", diff --git a/docs/src/adapters/phpstan.md b/docs/src/adapters/phpstan.md index 45ffc09..a6e5c81 100644 --- a/docs/src/adapters/phpstan.md +++ b/docs/src/adapters/phpstan.md @@ -82,7 +82,7 @@ specification that can never match is exactly the mistake a green suite hides. | `understudy.cardinality` | `times(5, 2)`, a negative bound, `verify(…, never: true, times: 3)`, `times` beside a `minimum` | | `understudy.matcher` | a matcher whose kind the parameter can never accept: `Arg::int()` where a `string` is declared | | `understudy.returns` | `returns()` on a method declared `void`, where no value is ever observed | -| `understudy.matcherLeak` | a matcher written outside a specification, where it reaches the code as a value | +| `understudy.matcherLeak` | a matcher written outside a specification and outside any closure, where it reaches the code as a value; one hoisted into a variable, stored on a property or written in a closure handed over later is not one | To silence one, use its identifier: diff --git a/docs/src/adapters/phpunit.md b/docs/src/adapters/phpunit.md index d028b86..80fc639 100644 --- a/docs/src/adapters/phpunit.md +++ b/docs/src/adapters/phpunit.md @@ -24,7 +24,7 @@ Pest works too — it runs on PHPUnit, so the same trait applies through | **Verify after success** | after a body that reaches `assertPostConditions()`, every `expect()` is checked. An expectation the code never fulfilled fails the test as an assertion failure | | **Original failure wins** | after a failing body nothing is verified, so the adapter can never mask the error that actually happened | | **Reset always** | an `#[After]` hook drops the context unconditionally. One test can never leak a double into the next | -| **Early guard** | a `#[Before]` hook refuses to start over a context some earlier test left behind, which is what broken integration looks like | +| **Early guard** | a `#[Before]` hook refuses to start over a context some earlier test left behind, which is what broken integration looks like — or one that `setUpBeforeClass()` filled: the context lives for one test, so create doubles in `setUp()` | ## Using it @@ -79,6 +79,15 @@ abstract class ProjectTestCase extends TestCase See [Strict stubs](/guide/expectations/strict-stubs). +**Verification runs before your teardown here, and after it under +[Testo](/adapters/testo).** `assertPostConditions()` is called by PHPUnit +*before* `tearDown()`; the Testo interceptor runs outside `#[AfterTest]`. +Neither is wrong, but a test whose expectation is fulfilled by teardown itself +fails here and passes there. `reset()` runs after teardown in both. + +A test that creates no double is not touched at all: nothing is counted for it, +so `#[DoesNotPerformAssertions]` keeps meaning what it says. + ## Overriding `assertPostConditions()` PHP resolves a method-name conflict between a class and a trait **silently** in diff --git a/docs/src/adapters/psalm.md b/docs/src/adapters/psalm.md index 8fc8d55..5de5fb4 100644 --- a/docs/src/adapters/psalm.md +++ b/docs/src/adapters/psalm.md @@ -34,6 +34,17 @@ report inside a specification closure, and **only** there: The last row is the point. A matcher reaching a real call raises `MatcherLeaked` at run time, and a plugin that hid it would be worse than no plugin at all. +::: warning What reports that last row is Psalm, and it needs `errorLevel="1"` +The plugin suppresses; the report it leaves standing is Psalm's own +`MixedArgument`, which exists only at level 1. At levels 2 and above a leaked +matcher draws nothing here — the runtime `MatcherLeaked` catches it, one test +run later. The [PHPStan extension](/adapters/phpstan) has a rule of its own +(`understudy.matcherLeak`) and reports at every level; this plugin deliberately +does not, because a rule strict enough to catch a leak textually also misreads a +matcher that reaches its specification through a variable, a property or a +helper. +::: + ## The two 0.4 idioms - **`Arg::rest()`** legitimately passes fewer arguments than the contract diff --git a/docs/src/adapters/testo.md b/docs/src/adapters/testo.md index 474b287..06a92ec 100644 --- a/docs/src/adapters/testo.md +++ b/docs/src/adapters/testo.md @@ -94,7 +94,9 @@ A test whose only check is an understudy expectation is **not** risky. Testo calls a passing test risky when it recorded no assertion, and it decides that before this adapter can contribute the verification — so the adapter takes the verdict back when its own record is the only one in the history. Tests that -also assert on their own keep whatever verdict they earned. +also assert on their own keep whatever verdict they earned, and **a test that +created no double is not touched at all**: nothing is recorded for it, its +assertion count is its own, and the runner's verdict stands. ::: warning One place it is not visible The `assert-history` block Testo prints. The collector renders that text before @@ -125,6 +127,12 @@ returned is still referenced while your `#[AfterTest]` runs. For a value that owns an OS resource this matters, and it has bitten a real suite. See [Retention and lean()](/guide/lifecycle/retention). +Verification runs after your teardown here too — in the interceptor, outside +`#[AfterTest]` — while the [PHPUnit adapter](/adapters/phpunit) verifies in +`assertPostConditions()`, which PHPUnit calls *before* `tearDown()`. Neither is +wrong, but a test whose expectation is fulfilled by teardown itself passes here +and fails there. + ## API | Member | Purpose | diff --git a/docs/src/api/classes/Arg.md b/docs/src/api/classes/Arg.md index c64a072..e1139a4 100644 --- a/docs/src/api/classes/Arg.md +++ b/docs/src/api/classes/Arg.md @@ -9,7 +9,7 @@ description: "Argument matchers, usable only inside a specification closure:" `Rasuvaeff\Understudy\Arg` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Arg.php#L48) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Arg.php#L49) — **Version:** v0.8.0 Argument matchers, usable only inside a specification closure: @@ -72,6 +72,8 @@ Matches a string, optionally against a PCRE pattern. static bool(): mixed ``` +Matches a `bool`, either value. + ### same() ```php diff --git a/docs/src/api/classes/Captor.md b/docs/src/api/classes/Captor.md index 6e8049e..88469ce 100644 --- a/docs/src/api/classes/Captor.md +++ b/docs/src/api/classes/Captor.md @@ -9,7 +9,7 @@ description: "A typed argument captor, built by Arg::captor()." `Rasuvaeff\Understudy\Captor` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Captor.php#L38) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Captor.php#L38) — **Version:** v0.8.0 **Type parameters:** diff --git a/docs/src/api/classes/Cardinality.md b/docs/src/api/classes/Cardinality.md deleted file mode 100644 index 4f36ea9..0000000 --- a/docs/src/api/classes/Cardinality.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: "Cardinality" -description: "How many times a call is allowed to happen." ---- - - - -# `Cardinality` - -`Rasuvaeff\Understudy\Cardinality` - -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Cardinality.php#L15) — **Version:** v0.7.2 - -How many times a call is allowed to happen. `null` as the maximum means no -upper bound. - -## Constructor - -```php -__construct( - int<0, max> $minimum, - int<0, max>|null $maximum, -) -``` - -| Parameter | Type | Default | Description | -|---|---|---|---| -| `$minimum` | `int<0, max>` | *required* | | -| `$maximum` | `int<0, max>\|null` | *required* | | - -## Methods - -### exactly() - -```php -static exactly(int $times): Cardinality -``` - -### atLeast() - -```php -static atLeast(int $minimum): Cardinality -``` - -### between() - -```php -static between(int $minimum, ?int $maximum): Cardinality -``` - -### any() - -```php -static any(): Cardinality -``` - -Any number of calls, including none. - -### never() - -```php -static never(): Cardinality -``` - -### allows() - -```php -allows(int<0, max> $count): bool -``` - -### describe() - -```php -describe(): string -``` - diff --git a/docs/src/api/classes/Exception/AmbiguousDefaultFactory.md b/docs/src/api/classes/Exception/AmbiguousDefaultFactory.md index 084d32a..54d4031 100644 --- a/docs/src/api/classes/Exception/AmbiguousDefaultFactory.md +++ b/docs/src/api/classes/Exception/AmbiguousDefaultFactory.md @@ -9,7 +9,7 @@ description: "Two registered default factories are equally close to the requeste `Rasuvaeff\Understudy\Exception\AmbiguousDefaultFactory` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/AmbiguousDefaultFactory.php#L12) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/AmbiguousDefaultFactory.php#L12) — **Version:** v0.8.0 **Extends:** `LogicException` diff --git a/docs/src/api/classes/Exception/BypassUnavailable.md b/docs/src/api/classes/Exception/BypassUnavailable.md index a48c4f5..c8fa7b2 100644 --- a/docs/src/api/classes/Exception/BypassUnavailable.md +++ b/docs/src/api/classes/Exception/BypassUnavailable.md @@ -9,7 +9,7 @@ description: "`bypassFinals()` cannot do what was asked of it." `Rasuvaeff\Understudy\Exception\BypassUnavailable` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/BypassUnavailable.php#L12) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/BypassUnavailable.php#L12) — **Version:** v0.8.0 **Extends:** `LogicException` @@ -37,3 +37,6 @@ static notAClass(class-string $class, string $kind): Exception\BypassUnavailable static foreignWrapper(string $owner): Exception\BypassUnavailable ``` +Something else already transforms PHP source on `file://`; replacing it +would silently disable whatever it does. + diff --git a/docs/src/api/classes/Exception/CannotWire.md b/docs/src/api/classes/Exception/CannotWire.md index a9af0c6..1e689b0 100644 --- a/docs/src/api/classes/Exception/CannotWire.md +++ b/docs/src/api/classes/Exception/CannotWire.md @@ -9,7 +9,7 @@ description: "`wire()` cannot build the subject, or cannot decide what to pass i `Rasuvaeff\Understudy\Exception\CannotWire` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/CannotWire.php#L12) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/CannotWire.php#L12) — **Version:** v0.8.0 **Extends:** `InvalidArgumentException` diff --git a/docs/src/api/classes/Exception/ConflictingExpectation.md b/docs/src/api/classes/Exception/ConflictingExpectation.md index 5e8461f..732f4ad 100644 --- a/docs/src/api/classes/Exception/ConflictingExpectation.md +++ b/docs/src/api/classes/Exception/ConflictingExpectation.md @@ -9,7 +9,7 @@ description: "A `when()` or `expect()` names a call another registration already `Rasuvaeff\Understudy\Exception\ConflictingExpectation` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/ConflictingExpectation.php#L21) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/ConflictingExpectation.php#L21) — **Version:** v0.8.0 **Extends:** `LogicException` diff --git a/docs/src/api/classes/Exception/ContextOwnershipViolation.md b/docs/src/api/classes/Exception/ContextOwnershipViolation.md index ed2d584..508f081 100644 --- a/docs/src/api/classes/Exception/ContextOwnershipViolation.md +++ b/docs/src/api/classes/Exception/ContextOwnershipViolation.md @@ -9,7 +9,7 @@ description: "Configuration and verification belong to the context that created `Rasuvaeff\Understudy\Exception\ContextOwnershipViolation` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/ContextOwnershipViolation.php#L14) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/ContextOwnershipViolation.php#L14) — **Version:** v0.8.0 **Extends:** `LogicException` @@ -28,3 +28,6 @@ make lifecycle verification race with work owned elsewhere. static forDouble(): Exception\ContextOwnershipViolation ``` +A double was configured or verified from a context other than the one +that created it — another Fiber, or outside the scope it was built in. + diff --git a/docs/src/api/classes/Exception/ForgottenDouble.md b/docs/src/api/classes/Exception/ForgottenDouble.md index fe70b5f..7c9eb67 100644 --- a/docs/src/api/classes/Exception/ForgottenDouble.md +++ b/docs/src/api/classes/Exception/ForgottenDouble.md @@ -9,7 +9,7 @@ description: "A double outlived the context that created it — almost always a `Rasuvaeff\Understudy\Exception\ForgottenDouble` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/ForgottenDouble.php#L16) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/ForgottenDouble.php#L16) — **Version:** v0.8.0 **Extends:** `LogicException` @@ -47,6 +47,9 @@ static propertyAfterReset(non-empty-string $property): Exception\ForgottenDouble static retired(): Exception\ForgottenDouble ``` +A facade — `strict()`, `verify()`, `nothingElse()`, `forget()` itself — +was asked about a double that `Understudy::forget()` retired. + ### fromDefaultFactory() ```php diff --git a/docs/src/api/classes/Exception/ForwardingTargetMismatch.md b/docs/src/api/classes/Exception/ForwardingTargetMismatch.md index 20b109d..c75f8a3 100644 --- a/docs/src/api/classes/Exception/ForwardingTargetMismatch.md +++ b/docs/src/api/classes/Exception/ForwardingTargetMismatch.md @@ -9,7 +9,7 @@ description: "The instance offered as a forwarding target does not satisfy what `Rasuvaeff\Understudy\Exception\ForwardingTargetMismatch` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/ForwardingTargetMismatch.php#L13) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/ForwardingTargetMismatch.php#L13) — **Version:** v0.8.0 **Extends:** `InvalidArgumentException` diff --git a/docs/src/api/classes/Exception/InvalidCallSpecification.md b/docs/src/api/classes/Exception/InvalidCallSpecification.md index 92b81d5..45b1f31 100644 --- a/docs/src/api/classes/Exception/InvalidCallSpecification.md +++ b/docs/src/api/classes/Exception/InvalidCallSpecification.md @@ -1,6 +1,6 @@ --- title: "InvalidCallSpecification" -description: "The closure handed to when()/verify()/calls() did not describe one call the way a specification must: no direct call on an understudy, more than one, or…" +description: "A specification of the wrong SHAPE: the closure handed to when()/verify()/ calls() made no direct call on an understudy, made it on a static method or…" --- @@ -9,16 +9,26 @@ description: "The closure handed to when()/verify()/calls() did not describe one `Rasuvaeff\Understudy\Exception\InvalidCallSpecification` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidCallSpecification.php#L15) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidCallSpecification.php#L25) — **Version:** v0.8.0 **Extends:** `LogicException` **Implements:** `Throwable`, `Stringable`, [`Exception\UnderstudyError`](/api/classes/Exception/UnderstudyError) -The closure handed to when()/verify()/calls() did not describe one call the -way a specification must: no direct call on an understudy, more than one, or -arguments that cannot form a valid specification — which includes a matcher -configured so that it could never match anything. +A specification of the wrong SHAPE: the closure handed to when()/verify()/ +calls() made no direct call on an understudy, made it on a static method or +threw first; a tail matcher was not the last argument; the specification +stopped before the required parameters ran out without saying so with +`Arg::rest()`; a combinator was given nothing to combine; `verify()` was +handed arguments that contradict each other; a facade was handed an object +that is not a double; a protocol was armed over one still running. + +The line between this class and [`Exception\InvalidSpecificationArgument`](/api/classes/Exception/InvalidSpecificationArgument): this +one is about how a specification is written — what is called, where, how +many times — and that one about a VALUE inside it that no run could act on +(an inverted range, a count below zero, a pattern PCRE cannot compile, a +type that is not loadable). A `LogicException`, because the test is +malformed rather than given a bad number. ## Methods @@ -28,6 +38,9 @@ configured so that it could never match anything. static noCallRecorded(): Exception\InvalidCallSpecification ``` +The closure ran to its end without any generated method signalling: it +called nothing on a double, or only something that is not one. + ### notADouble() ```php @@ -57,6 +70,9 @@ recognise a second phrasing of the same mistake afterwards. static exactCountBesideABound(): Exception\InvalidCallSpecification ``` +`verify($call, times: 2, minimum: 1)`: an exact count leaves a bound +nothing to constrain, so one of the two is a mistake. + ### misplacedTailMatcher() ```php @@ -73,37 +89,6 @@ static misplacedTailMatcher( static emptyCombinator(non-empty-string $matcher): Exception\InvalidCallSpecification ``` -### invertedBounds() - -```php -static invertedBounds( - non-empty-string $matcher, - int|float $minimum, - int|float $maximum, -): Exception\InvalidCallSpecification -``` - -A range matcher whose maximum sits below its minimum, so it describes an -empty range and could never match. - -- `$matcher` — the factory that was called, without `Arg::` -- `$minimum` — the lower bound as it was given -- `$maximum` — the upper bound as it was given - -### invalidPattern() - -```php -static invalidPattern( - non-empty-string $pattern, - non-empty-string|null $reason, -): Exception\InvalidCallSpecification -``` - -A pattern handed to `Arg::string()` that PCRE cannot compile. - -- `$pattern` — the pattern as it was written -- `$reason` — what PCRE said, when it said anything - ### tailMatcherInCombinator() ```php @@ -119,6 +104,9 @@ static tailMatcherInCombinator( static emptySequence(): Exception\InvalidCallSpecification ``` +`expectSequence()` with no steps: arming an empty protocol would put every +later call on trial with nothing to try it against. + ### protocolAlreadyArmed() ```php @@ -163,6 +151,9 @@ static omittedTailNeedsRest( static closureFailed(Throwable $previous): Exception\InvalidCallSpecification ``` +The specification closure threw before any generated method signalled; +the original is kept as `previous`, because it is the actual mistake. + ### staticMethodCalled() ```php diff --git a/docs/src/api/classes/Exception/InvalidDefaultValue.md b/docs/src/api/classes/Exception/InvalidDefaultValue.md index 679b85a..c2feb38 100644 --- a/docs/src/api/classes/Exception/InvalidDefaultValue.md +++ b/docs/src/api/classes/Exception/InvalidDefaultValue.md @@ -9,7 +9,7 @@ description: "A registered default factory produced a value the contract cannot `Rasuvaeff\Understudy\Exception\InvalidDefaultValue` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidDefaultValue.php#L12) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidDefaultValue.php#L12) — **Version:** v0.8.0 **Extends:** `RuntimeException` diff --git a/docs/src/api/classes/Exception/InvalidSpecificationArgument.md b/docs/src/api/classes/Exception/InvalidSpecificationArgument.md index 8314425..6f04990 100644 --- a/docs/src/api/classes/Exception/InvalidSpecificationArgument.md +++ b/docs/src/api/classes/Exception/InvalidSpecificationArgument.md @@ -1,6 +1,6 @@ --- title: "InvalidSpecificationArgument" -description: "An argument to a specification that no run could act on: a maximum call count below its minimum, a negative count, `returns()` with nothing to return." +description: "A VALUE inside a specification that no run could act on: a maximum call count below its minimum, a negative count, `returns()` with nothing to return, a…" --- @@ -9,23 +9,25 @@ description: "An argument to a specification that no run could act on: a maximum `Rasuvaeff\Understudy\Exception\InvalidSpecificationArgument` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidSpecificationArgument.php#L22) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/InvalidSpecificationArgument.php#L24) — **Version:** v0.8.0 **Extends:** `InvalidArgumentException` **Implements:** `Stringable`, `Throwable`, [`Exception\UnderstudyError`](/api/classes/Exception/UnderstudyError) -An argument to a specification that no run could act on: a maximum call +A VALUE inside a specification that no run could act on: a maximum call count below its minimum, a negative count, `returns()` with nothing to -return. - -Extends `\InvalidArgumentException` because that is what these three paths -have always thrown, and a user's `catch (\InvalidArgumentException $e)` -around them must keep working. It implements `UnderstudyError` because that -interface says it is implemented by every exception this library throws, -and these three were the exceptions to that — so a `catch (UnderstudyError -$e)`, which the documentation recommends for catching misuse of Understudy -itself, walked straight past them. +return, a matcher range whose maximum sits below its minimum, a pattern +PCRE cannot compile, `Arg::instanceOf()` naming a type that is not +loadable. + +The line between this class and [`Exception\InvalidCallSpecification`](/api/classes/Exception/InvalidCallSpecification): that one +is about the SHAPE of a specification — what is called, where, how many +times — and this one about a number, a pattern or a name given to it. It +extends `\InvalidArgumentException` because that is the SPL type these +paths have always been, and a user's `catch (\InvalidArgumentException $e)` +around them keeps working; it implements `UnderstudyError` because that +interface is implemented by every exception this library throws. ## Methods @@ -75,3 +77,34 @@ only as an expectation that was never met. - `$type` — the name as written +### invertedBounds() + +```php +static invertedBounds( + non-empty-string $matcher, + int|float $minimum, + int|float $maximum, +): Exception\InvalidSpecificationArgument +``` + +A range matcher whose maximum sits below its minimum, so it describes an +empty range and could never match. + +- `$matcher` — the factory that was called, without `Arg::` +- `$minimum` — the lower bound as it was given +- `$maximum` — the upper bound as it was given + +### invalidPattern() + +```php +static invalidPattern( + non-empty-string $pattern, + non-empty-string|null $reason, +): Exception\InvalidSpecificationArgument +``` + +A pattern handed to `Arg::string()` that PCRE cannot compile. + +- `$pattern` — the pattern as it was written +- `$reason` — what PCRE said, when it said anything + diff --git a/docs/src/api/classes/Exception/MatcherLeaked.md b/docs/src/api/classes/Exception/MatcherLeaked.md index 71228ea..209f0f4 100644 --- a/docs/src/api/classes/Exception/MatcherLeaked.md +++ b/docs/src/api/classes/Exception/MatcherLeaked.md @@ -9,7 +9,7 @@ description: "A matcher reached a real call instead of a specification closure." `Rasuvaeff\Understudy\Exception\MatcherLeaked` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/MatcherLeaked.php#L14) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/MatcherLeaked.php#L14) — **Version:** v0.8.0 **Extends:** `LogicException` diff --git a/docs/src/api/classes/Exception/NeverMethodCalled.md b/docs/src/api/classes/Exception/NeverMethodCalled.md index 4ddf24d..63452a9 100644 --- a/docs/src/api/classes/Exception/NeverMethodCalled.md +++ b/docs/src/api/classes/Exception/NeverMethodCalled.md @@ -9,7 +9,7 @@ description: "A method declared `: never` was called without an expectation that `Rasuvaeff\Understudy\Exception\NeverMethodCalled` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/NeverMethodCalled.php#L14) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/NeverMethodCalled.php#L14) — **Version:** v0.8.0 **Extends:** `RuntimeException` diff --git a/docs/src/api/classes/Exception/NoDefaultValue.md b/docs/src/api/classes/Exception/NoDefaultValue.md index 48cfb9a..d88f29c 100644 --- a/docs/src/api/classes/Exception/NoDefaultValue.md +++ b/docs/src/api/classes/Exception/NoDefaultValue.md @@ -9,7 +9,7 @@ description: "A loose understudy had to answer a call, but the declared return t `Rasuvaeff\Understudy\Exception\NoDefaultValue` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/NoDefaultValue.php#L14) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/NoDefaultValue.php#L14) — **Version:** v0.8.0 **Extends:** `RuntimeException` diff --git a/docs/src/api/classes/Exception/NothingCaptured.md b/docs/src/api/classes/Exception/NothingCaptured.md index b03620c..c5ae057 100644 --- a/docs/src/api/classes/Exception/NothingCaptured.md +++ b/docs/src/api/classes/Exception/NothingCaptured.md @@ -9,7 +9,7 @@ description: "`Captor::last()` was read before any matched call carried a value `Rasuvaeff\Understudy\Exception\NothingCaptured` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/NothingCaptured.php#L13) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/NothingCaptured.php#L13) — **Version:** v0.8.0 **Extends:** `LogicException` diff --git a/docs/src/api/classes/Exception/OriginalCallUnavailable.md b/docs/src/api/classes/Exception/OriginalCallUnavailable.md index 75e30a1..541e68f 100644 --- a/docs/src/api/classes/Exception/OriginalCallUnavailable.md +++ b/docs/src/api/classes/Exception/OriginalCallUnavailable.md @@ -9,7 +9,7 @@ description: "`callOriginal()` was asked to delegate, and there is nothing to de `Rasuvaeff\Understudy\Exception\OriginalCallUnavailable` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/OriginalCallUnavailable.php#L12) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/OriginalCallUnavailable.php#L12) — **Version:** v0.8.0 **Extends:** `LogicException` diff --git a/docs/src/api/classes/Exception/OriginalReturnTypeViolation.md b/docs/src/api/classes/Exception/OriginalReturnTypeViolation.md index 175bf00..f4dce3b 100644 --- a/docs/src/api/classes/Exception/OriginalReturnTypeViolation.md +++ b/docs/src/api/classes/Exception/OriginalReturnTypeViolation.md @@ -9,7 +9,7 @@ description: "A forwarded call returned an object the double cannot stand in for `Rasuvaeff\Understudy\Exception\OriginalReturnTypeViolation` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/OriginalReturnTypeViolation.php#L12) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/OriginalReturnTypeViolation.php#L12) — **Version:** v0.8.0 **Extends:** `RuntimeException` diff --git a/docs/src/api/classes/Exception/OutcomeUnavailable.md b/docs/src/api/classes/Exception/OutcomeUnavailable.md index e360c84..62b8681 100644 --- a/docs/src/api/classes/Exception/OutcomeUnavailable.md +++ b/docs/src/api/classes/Exception/OutcomeUnavailable.md @@ -9,7 +9,7 @@ description: "An invocation's outcome was read as the wrong kind: a returned val `Rasuvaeff\Understudy\Exception\OutcomeUnavailable` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/OutcomeUnavailable.php#L14) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/OutcomeUnavailable.php#L14) — **Version:** v0.8.0 **Extends:** `LogicException` diff --git a/docs/src/api/classes/Exception/StrictModeViolation.md b/docs/src/api/classes/Exception/StrictModeViolation.md index d2e4f83..7645bf0 100644 --- a/docs/src/api/classes/Exception/StrictModeViolation.md +++ b/docs/src/api/classes/Exception/StrictModeViolation.md @@ -9,7 +9,7 @@ description: "A strict understudy received a call no expectation matched." `Rasuvaeff\Understudy\Exception\StrictModeViolation` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/StrictModeViolation.php#L12) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/StrictModeViolation.php#L12) — **Version:** v0.8.0 **Extends:** `RuntimeException` diff --git a/docs/src/api/classes/Exception/UnderstudyError.md b/docs/src/api/classes/Exception/UnderstudyError.md index 34e7e61..4861d70 100644 --- a/docs/src/api/classes/Exception/UnderstudyError.md +++ b/docs/src/api/classes/Exception/UnderstudyError.md @@ -9,7 +9,7 @@ description: "Implemented by every exception this library throws, so a test can `Rasuvaeff\Understudy\Exception\UnderstudyError` -**Interface** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/UnderstudyError.php#L14) — **Version:** v0.7.2 +**Interface** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/UnderstudyError.php#L14) — **Version:** v0.8.0 **Implements:** `Throwable`, `Stringable` diff --git a/docs/src/api/classes/Exception/UnsupportedTarget.md b/docs/src/api/classes/Exception/UnsupportedTarget.md index 20787f8..713dfe6 100644 --- a/docs/src/api/classes/Exception/UnsupportedTarget.md +++ b/docs/src/api/classes/Exception/UnsupportedTarget.md @@ -9,7 +9,7 @@ description: "The requested target cannot be doubled, and no option would make i `Rasuvaeff\Understudy\Exception\UnsupportedTarget` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/UnsupportedTarget.php#L13) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/UnsupportedTarget.php#L13) — **Version:** v0.8.0 **Extends:** `LogicException` @@ -26,6 +26,9 @@ without the user changing something. The message names that something. static missing(string $target): Exception\UnsupportedTarget ``` +No class or interface of that name is loadable — usually a typo, or a +class the autoloader cannot see from the test. + ### notDoublable() ```php diff --git a/docs/src/api/classes/Exception/VerificationFailed.md b/docs/src/api/classes/Exception/VerificationFailed.md index 7352b79..be737e7 100644 --- a/docs/src/api/classes/Exception/VerificationFailed.md +++ b/docs/src/api/classes/Exception/VerificationFailed.md @@ -9,7 +9,7 @@ description: "A verification about what the code under test did (or did not) do `Rasuvaeff\Understudy\Exception\VerificationFailed` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/VerificationFailed.php#L20) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Exception/VerificationFailed.php#L20) — **Version:** v0.8.0 **Extends:** `RuntimeException` diff --git a/docs/src/api/classes/ExpectBuilder.md b/docs/src/api/classes/ExpectBuilder.md index 42b208b..1034083 100644 --- a/docs/src/api/classes/ExpectBuilder.md +++ b/docs/src/api/classes/ExpectBuilder.md @@ -9,7 +9,7 @@ description: "Configures a call the code under test is expected to make." `Rasuvaeff\Understudy\ExpectBuilder` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/ExpectBuilder.php#L19) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/ExpectBuilder.php#L19) — **Version:** v0.8.0 **Extends:** [`WhenBuilder`](/api/classes/WhenBuilder) diff --git a/docs/src/api/classes/FailureKind.md b/docs/src/api/classes/FailureKind.md index 14dafa6..777775b 100644 --- a/docs/src/api/classes/FailureKind.md +++ b/docs/src/api/classes/FailureKind.md @@ -9,7 +9,7 @@ description: "What kind of verification claim a VerificationFailure reports." `Rasuvaeff\Understudy\FailureKind` -**Enum** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/FailureKind.php#L12) — **Version:** v0.7.2 +**Enum** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/FailureKind.php#L12) — **Version:** v0.8.0 **Implements:** `UnitEnum` diff --git a/docs/src/api/classes/Invocation.md b/docs/src/api/classes/Invocation.md index dc8723c..59e944e 100644 --- a/docs/src/api/classes/Invocation.md +++ b/docs/src/api/classes/Invocation.md @@ -9,7 +9,7 @@ description: "One recorded call on an understudy." `Rasuvaeff\Understudy\Invocation` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Invocation.php#L20) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Invocation.php#L20) — **Version:** v0.8.0 One recorded call on an understudy. @@ -90,6 +90,10 @@ never ran, so the parent body would work over state that does not exist. recordOutcome(Outcome $outcome): void ``` +The wrapped form of recordReturned() and recordThrown(), +kept for the callers that still hand over an `Outcome`; dispatch itself +uses the scalar recorders because every call reaches them. + ### recordReturned() ```php @@ -144,21 +148,35 @@ isAccounted(): bool didReturn(): bool ``` +Whether the call answered with a value — `null` included, which is why +this is asked rather than inferred from returned(). + ### didThrow() ```php didThrow(): bool ``` +Whether the call ended in a throwable, which thrown() then holds. + ### returned() ```php returned(): mixed ``` +The value the call answered with. + +**Throws:** + +- [`Exception\OutcomeUnavailable`](/api/classes/Exception/OutcomeUnavailable) — when the call threw instead, or when the +understudy is lean and did not keep the value + ### thrown() ```php thrown(): ?Throwable ``` +The throwable the call ended in, or null when it returned. + diff --git a/docs/src/api/classes/Outcome.md b/docs/src/api/classes/Outcome.md deleted file mode 100644 index 9ff7de4..0000000 --- a/docs/src/api/classes/Outcome.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: "Outcome" -description: "How one call ended: with a value or with a throwable." ---- - - - -# `Outcome` - -`Rasuvaeff\Understudy\Outcome` - -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Outcome.php#L16) — **Version:** v0.7.2 - -How one call ended: with a value or with a throwable. Kept as its own type -because `null` is a perfectly valid return value and cannot double as -"nothing was returned". - -## Constructor - -```php -__construct( - bool $returned, - mixed $value, - ?Throwable $thrown, -) -``` - -| Parameter | Type | Default | Description | -|---|---|---|---| -| `$returned` | `bool` | *required* | | -| `$value` | `mixed` | *required* | | -| `$thrown` | `?Throwable` | *required* | | - -## Methods - -### returnedValue() - -```php -static returnedValue(mixed $value): Outcome -``` - -### thrownError() - -```php -static thrownError(Throwable $thrown): Outcome -``` - -### didReturn() - -```php -didReturn(): bool -``` - -### didThrow() - -```php -didThrow(): bool -``` - -### returned() - -```php -returned(non-empty-string $method): mixed -``` - -- `$method` — used only to render a helpful message - -### thrown() - -```php -thrown(): ?Throwable -``` - diff --git a/docs/src/api/classes/PhpUnit/UnderstudyPHPUnitIntegration.md b/docs/src/api/classes/PhpUnit/UnderstudyPHPUnitIntegration.md index 0903ae4..4c8c103 100644 --- a/docs/src/api/classes/PhpUnit/UnderstudyPHPUnitIntegration.md +++ b/docs/src/api/classes/PhpUnit/UnderstudyPHPUnitIntegration.md @@ -9,7 +9,7 @@ description: "Ends every PHPUnit test with understudy's own bookkeeping done for `Rasuvaeff\Understudy\PhpUnit\UnderstudyPHPUnitIntegration` -**undefined** — **Package:** [rasuvaeff/understudy-phpunit](https://github.com/rasuvaeff/understudy-phpunit) — [Source](https://github.com/rasuvaeff/understudy-phpunit/blob/d8389a7e82bfc020c02cbe37122593f8f949d664/src/PhpUnit/UnderstudyPHPUnitIntegration.php#L65) — **Version:** v0.1.3 +**undefined** — **Package:** [rasuvaeff/understudy-phpunit](https://github.com/rasuvaeff/understudy-phpunit) — [Source](https://github.com/rasuvaeff/understudy-phpunit/blob/d48d08dcb2d23a0359d2f756cc51e96c8ece7624/src/PhpUnit/UnderstudyPHPUnitIntegration.php#L83) — **Version:** v0.1.7 Ends every PHPUnit test with understudy's own bookkeeping done for it. @@ -21,15 +21,30 @@ final class CheckoutTest extends TestCase public function testChargesForTheCart(): void { $books = Understudy::for(BookRepositoryInterface::class); - when(fn () => $books->find(7))->returns(new Book(7)); + expect(fn () => $books->find(7))->returns($expected = new Book(7)); -(new Checkout($books))->charge([7]); +$receipt = (new Checkout($books))->charge([7]); -expect(fn () => $books->find(7)); // verified for you +self::assertSame($expected->price, $receipt->total); } } ``` +One registration says both things: `find(7)` must be called exactly once, +and it answers `$expected`. Two rules of the engine decide that shape, and +this snippet used to break both: + +- **Arm before the run.** An `expect()` counts only the calls that arrive + after it is declared. Written below the action it counts zero and fails + as "called never" about a call that did happen; to claim a call that has + already happened, use `verify()`. +- **One registration per call.** A `when()` stub and an `expect()` naming + the same call are two registrations of one call, and the engine answers + with `ConflictingExpectation`. + +Kept in step with the README's Usage section and `examples/readme-usage.php`, +which runs it; `DocumentedUsageTest` fails when the three drift apart. + On a test that reached `PHPUnit\Framework\TestCase`::assertPostConditions() — that is, passed its body — the whole context is verified: an `expect()` the code never fulfilled fails the test as an assertion failure. A test whose body @@ -52,10 +67,13 @@ use Rasuvaeff\Understudy\PhpUnit\UnderstudyPHPUnitIntegration { protected function assertPostConditions(): void { - $this->understudyAssertPostConditions(); // your post-conditions ... + $this->understudyAssertPostConditions(); } ``` +The user's post-conditions run first — the check closer to the test body +wins, same as the README says. + No public members beyond what is documented above. diff --git a/docs/src/api/classes/Testo/UnderstudyInterceptor.md b/docs/src/api/classes/Testo/UnderstudyInterceptor.md index f5ad186..c9906ea 100644 --- a/docs/src/api/classes/Testo/UnderstudyInterceptor.md +++ b/docs/src/api/classes/Testo/UnderstudyInterceptor.md @@ -9,7 +9,7 @@ description: "Ends every test with understudy's own bookkeeping done for it." `Rasuvaeff\Understudy\Testo\UnderstudyInterceptor` -**Class** — **Package:** [rasuvaeff/understudy-testo](https://github.com/rasuvaeff/understudy-testo) — [Source](https://github.com/rasuvaeff/understudy-testo/blob/2ff72ea1c4400953bf9437655ceb1df1d02ca350/src/Testo/UnderstudyInterceptor.php#L51) — **Version:** v0.1.3 +**Class** — **Package:** [rasuvaeff/understudy-testo](https://github.com/rasuvaeff/understudy-testo) — [Source](https://github.com/rasuvaeff/understudy-testo/blob/dead543cd518323bf94aa1c7df69b947c71a40e9/src/Testo/UnderstudyInterceptor.php#L51) — **Version:** v0.1.4 **Implements:** `Testo\Pipeline\Middleware\TestRunInterceptor`, `Testo\Pipeline\Interceptor` diff --git a/docs/src/api/classes/Testo/UnderstudyPlugin.md b/docs/src/api/classes/Testo/UnderstudyPlugin.md index 5de076c..2841412 100644 --- a/docs/src/api/classes/Testo/UnderstudyPlugin.md +++ b/docs/src/api/classes/Testo/UnderstudyPlugin.md @@ -9,7 +9,7 @@ description: "Registers UnderstudyInterceptor so every test of the suite ends wi `Rasuvaeff\Understudy\Testo\UnderstudyPlugin` -**Class** — **Package:** [rasuvaeff/understudy-testo](https://github.com/rasuvaeff/understudy-testo) — [Source](https://github.com/rasuvaeff/understudy-testo/blob/2ff72ea1c4400953bf9437655ceb1df1d02ca350/src/Testo/UnderstudyPlugin.php#L29) — **Version:** v0.1.3 +**Class** — **Package:** [rasuvaeff/understudy-testo](https://github.com/rasuvaeff/understudy-testo) — [Source](https://github.com/rasuvaeff/understudy-testo/blob/dead543cd518323bf94aa1c7df69b947c71a40e9/src/Testo/UnderstudyPlugin.php#L29) — **Version:** v0.1.4 **Implements:** `Testo\Common\PluginConfigurator` diff --git a/docs/src/api/classes/Understudy.md b/docs/src/api/classes/Understudy.md index 52ed7e9..c1882e6 100644 --- a/docs/src/api/classes/Understudy.md +++ b/docs/src/api/classes/Understudy.md @@ -9,7 +9,7 @@ description: "The whole public surface, as static methods so that an understudy `Rasuvaeff\Understudy\Understudy` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Understudy.php#L39) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/Understudy.php#L39) — **Version:** v0.8.0 The whole public surface, as static methods so that an understudy itself can stay free of service members: every one of them would be a name the doubled @@ -401,8 +401,13 @@ that outlives the scope, so it stays for those to check. static checkpoint(bool $strictStubs = false): void ``` -Verifies the current context and clears what has been settled, keeping -the understudies themselves — for a long test that runs in phases. +Verifies every context the test put understudies in and clears what has +been settled — the claims a matching `expect()` or a successful +`verify()` accounted for — keeping the understudies, their modes and +their labels, for a long test that runs in phases. A call covered by a +`when()` stub alone stays in the log, because `nothingElse()` still +reads it; `reset()`, `scope()` and `lean()` are the ways to let go of +everything. ### reset() diff --git a/docs/src/api/classes/VerificationFailure.md b/docs/src/api/classes/VerificationFailure.md index f2fe965..38bdec4 100644 --- a/docs/src/api/classes/VerificationFailure.md +++ b/docs/src/api/classes/VerificationFailure.md @@ -9,7 +9,7 @@ description: "The structured half of one verification failure — the same facts `Rasuvaeff\Understudy\VerificationFailure` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/VerificationFailure.php#L33) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/VerificationFailure.php#L34) — **Version:** v0.8.0 The structured half of one verification failure — the same facts the rendered message states, addressable by field. @@ -29,10 +29,11 @@ Which fields are set depends on the kind: | `UnaccountedCalls` | label | — | — | unaccounted calls | the unaccounted calls | — | | `UnusedDouble` | label | — | `0`/`0` | calls received | every call | — | -The readonly fields of this class, of [`FailureKind`](/api/classes/FailureKind), and of the -exceptions carrying them are frozen public API from v0.1.0: renaming, -removing or retyping any of them is a major-version change. New kinds and -newly-populated fields are additive and may arrive in a minor. +The readonly fields of this class and every existing [`FailureKind`](/api/classes/FailureKind) +case are stable: renaming, removing or retyping one is a major-version +change. A NEW kind, or a field newly populated for an existing kind, may +arrive in a minor — so match on `FailureKind` with a `default` arm; an +exhaustive `match` over the enum is not a supported way to consume it. ## Constructor diff --git a/docs/src/api/classes/WhenBuilder.md b/docs/src/api/classes/WhenBuilder.md index 28b21a0..6e5d924 100644 --- a/docs/src/api/classes/WhenBuilder.md +++ b/docs/src/api/classes/WhenBuilder.md @@ -9,7 +9,7 @@ description: "Configures what a stubbed call does." `Rasuvaeff\Understudy\WhenBuilder` -**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/WhenBuilder.php#L33) — **Version:** v0.7.2 +**Class** — **Package:** [rasuvaeff/understudy](https://github.com/rasuvaeff/understudy) — [Source](https://github.com/rasuvaeff/understudy/blob/master/src/WhenBuilder.php#L36) — **Version:** v0.8.0 **Type parameters:** @@ -25,10 +25,13 @@ cannot grow one without changing its contract. Not `final`, and the one class here that is not: `ExpectBuilder` extends it to add the cardinality verbs, and the two are one fluent vocabulary rather -than two. The cost is that `protected readonly Expectation` — an -`@internal` type — is reachable from a subclass a consumer could write. That -is tolerated rather than intended: nothing in the contract invites it, and -closing it would mean duplicating every action verb. +than two. For everyone else it is closed by contract, not by keyword: +subclassing is not supported, the `protected readonly Expectation` it +carries is an `@internal` type that may change in any release, and a subclass +reading it is on its own. The keyword is missing only because closing the +class would mean duplicating every action verb on `ExpectBuilder` — and a +`@final` tag is not an option either, since Psalm would then refuse +`ExpectBuilder` itself. ## Constructor @@ -59,6 +62,9 @@ the last one. throws(Throwable $error): static ``` +Throws this exact instance on the call — the same object every time the +link answers, which is what a test holding a reference to it expects. + ### answers() ```php diff --git a/docs/src/api/exceptions.md b/docs/src/api/exceptions.md index 9b7f261..30fdf63 100644 --- a/docs/src/api/exceptions.md +++ b/docs/src/api/exceptions.md @@ -18,9 +18,9 @@ Every `@api` type across all five packages that implements `Throwable`. | [`ContextOwnershipViolation`](/api/classes/Exception/ContextOwnershipViolation) | core | `LogicException` | Configuration and verification belong to the context that created a double. | | [`ForgottenDouble`](/api/classes/Exception/ForgottenDouble) | core | `LogicException` | A double outlived the context that created it — almost always a double kept in a static property… | | [`ForwardingTargetMismatch`](/api/classes/Exception/ForwardingTargetMismatch) | core | `InvalidArgumentException` | The instance offered as a forwarding target does not satisfy what the double stands in for. | -| [`InvalidCallSpecification`](/api/classes/Exception/InvalidCallSpecification) | core | `LogicException` | The closure handed to when()/verify()/calls() did not describe one call the way a specification… | +| [`InvalidCallSpecification`](/api/classes/Exception/InvalidCallSpecification) | core | `LogicException` | A specification of the wrong SHAPE: the closure handed to when()/verify()/ calls() made no direct… | | [`InvalidDefaultValue`](/api/classes/Exception/InvalidDefaultValue) | core | `RuntimeException` | A registered default factory produced a value the contract cannot hold. | -| [`InvalidSpecificationArgument`](/api/classes/Exception/InvalidSpecificationArgument) | core | `InvalidArgumentException` | An argument to a specification that no run could act on: a maximum call count below its minimum, a… | +| [`InvalidSpecificationArgument`](/api/classes/Exception/InvalidSpecificationArgument) | core | `InvalidArgumentException` | A VALUE inside a specification that no run could act on: a maximum call count below its minimum, a… | | [`MatcherLeaked`](/api/classes/Exception/MatcherLeaked) | core | `LogicException` | A matcher reached a real call instead of a specification closure. | | [`NeverMethodCalled`](/api/classes/Exception/NeverMethodCalled) | core | `RuntimeException` | A method declared `: never` was called without an expectation that throws. | | [`NoDefaultValue`](/api/classes/Exception/NoDefaultValue) | core | `RuntimeException` | A loose understudy had to answer a call, but the declared return type has no safe default. | diff --git a/docs/src/api/functions.md b/docs/src/api/functions.md index df3f05b..ec14aa6 100644 --- a/docs/src/api/functions.md +++ b/docs/src/api/functions.md @@ -64,7 +64,7 @@ expectSequence( ## verify() -`Rasuvaeff\Understudy\verify()` · [Source](https://github.com/rasuvaeff/understudy/blob/master/src/functions.php#L83) +`Rasuvaeff\Understudy\verify()` · [Source](https://github.com/rasuvaeff/understudy/blob/master/src/functions.php#L84) ```php verify(callable $call, int<0, max>|null $times = NULL, int<0, max>|null $minimum = NULL, int<0, max>|null $maximum = NULL, bool $never = false): void @@ -82,7 +82,7 @@ verify(fn () => $repository->recordView($book), times: 2); | `$times = NULL` | `int<0, max>\|null` | | | `$minimum = NULL` | `int<0, max>\|null` | | | `$maximum = NULL` | `int<0, max>\|null` | | -| `$never = false` | `bool` | | +| `$never = false` | `bool` | the call must not have happened at all; refused beside a count | ## when() diff --git a/docs/src/api/index.md b/docs/src/api/index.md index d65d410..92ce6cc 100644 --- a/docs/src/api/index.md +++ b/docs/src/api/index.md @@ -21,7 +21,6 @@ Generated by reflection (`docs/scripts/reflect-api.php`) over all five packages' |---|---|---| | [`Arg`](/api/classes/Arg) | class | Argument matchers, usable only inside a specification closure: | | [`Captor`](/api/classes/Captor) | class | A typed argument captor, built by Arg::captor(). | -| [`Cardinality`](/api/classes/Cardinality) | class | How many times a call is allowed to happen. | | [`AmbiguousDefaultFactory`](/api/classes/Exception/AmbiguousDefaultFactory) | class | Two registered default factories are equally close to the requested type. | | [`BypassUnavailable`](/api/classes/Exception/BypassUnavailable) | class | `bypassFinals()` cannot do what was asked of it. | | [`CannotWire`](/api/classes/Exception/CannotWire) | class | `wire()` cannot build the subject, or cannot decide what to pass it. | @@ -29,9 +28,9 @@ Generated by reflection (`docs/scripts/reflect-api.php`) over all five packages' | [`ContextOwnershipViolation`](/api/classes/Exception/ContextOwnershipViolation) | class | Configuration and verification belong to the context that created a double. | | [`ForgottenDouble`](/api/classes/Exception/ForgottenDouble) | class | A double outlived the context that created it — almost always a double kept in a static property… | | [`ForwardingTargetMismatch`](/api/classes/Exception/ForwardingTargetMismatch) | class | The instance offered as a forwarding target does not satisfy what the double stands in for. | -| [`InvalidCallSpecification`](/api/classes/Exception/InvalidCallSpecification) | class | The closure handed to when()/verify()/calls() did not describe one call the way a specification… | +| [`InvalidCallSpecification`](/api/classes/Exception/InvalidCallSpecification) | class | A specification of the wrong SHAPE: the closure handed to when()/verify()/ calls() made no direct… | | [`InvalidDefaultValue`](/api/classes/Exception/InvalidDefaultValue) | class | A registered default factory produced a value the contract cannot hold. | -| [`InvalidSpecificationArgument`](/api/classes/Exception/InvalidSpecificationArgument) | class | An argument to a specification that no run could act on: a maximum call count below its minimum, a… | +| [`InvalidSpecificationArgument`](/api/classes/Exception/InvalidSpecificationArgument) | class | A VALUE inside a specification that no run could act on: a maximum call count below its minimum, a… | | [`MatcherLeaked`](/api/classes/Exception/MatcherLeaked) | class | A matcher reached a real call instead of a specification closure. | | [`NeverMethodCalled`](/api/classes/Exception/NeverMethodCalled) | class | A method declared `: never` was called without an expectation that throws. | | [`NoDefaultValue`](/api/classes/Exception/NoDefaultValue) | class | A loose understudy had to answer a call, but the declared return type has no safe default. | @@ -46,7 +45,6 @@ Generated by reflection (`docs/scripts/reflect-api.php`) over all five packages' | [`ExpectBuilder`](/api/classes/ExpectBuilder) | class | Configures a call the code under test is expected to make. | | [`FailureKind`](/api/classes/FailureKind) | enum | What kind of verification claim a VerificationFailure reports. | | [`Invocation`](/api/classes/Invocation) | class | One recorded call on an understudy. | -| [`Outcome`](/api/classes/Outcome) | class | How one call ended: with a value or with a throwable. | | [`Understudy`](/api/classes/Understudy) | class | The whole public surface, as static methods so that an understudy itself can stay free of service… | | [`VerificationFailure`](/api/classes/VerificationFailure) | class | The structured half of one verification failure — the same facts the rendered message states,… | | [`WhenBuilder`](/api/classes/WhenBuilder) | class | Configures what a stubbed call does. | diff --git a/docs/src/api/rules.md b/docs/src/api/rules.md index 9efd72a..8562475 100644 --- a/docs/src/api/rules.md +++ b/docs/src/api/rules.md @@ -9,15 +9,15 @@ description: "Every identifier the PHPStan extension reports and every issue typ Neither analyser package has an `@api` class, and that is correct rather than a missing annotation: `understudy-phpstan` is registered through `extension.neon` and `understudy-psalm` through `psalm-plugin enable`, so a user never names one of their classes. What a user does write is an **identifier**, in `ignoreErrors` or a suppression. That is the contract, so that is what is reflected here. -## PHPStan · `rasuvaeff/understudy-phpstan` v0.2.0 +## PHPStan · `rasuvaeff/understudy-phpstan` v0.2.1 | Identifier | Reported by | Summary | |---|---|---| -| `understudy.cardinality` | [`SpecificationCheck`](https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/SpecificationCheck.php) | Says what the engine would say, before the test runs. | -| `understudy.closure` | [`SpecificationCheck`](https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/SpecificationCheck.php) | Says what the engine would say, before the test runs. | -| `understudy.matcher` | [`SpecificationCheck`](https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Internal/SpecificationCheck.php) | Says what the engine would say, before the test runs. | -| `understudy.matcherLeak` | [`MatcherLeakRule`](https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Rule/MatcherLeakRule.php) | A matcher written outside a specification. | -| `understudy.returns` | [`VoidReturnsRule`](https://github.com/rasuvaeff/understudy-phpstan/blob/911d4c47950803ea3e4fed74112598c1db09d640/src/PhpStan/Rule/VoidReturnsRule.php) | `returns()` on a method that returns nothing. | +| `understudy.cardinality` | [`SpecificationCheck`](https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/SpecificationCheck.php) | Says what the engine would say, before the test runs. | +| `understudy.closure` | [`SpecificationCheck`](https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/SpecificationCheck.php) | Says what the engine would say, before the test runs. | +| `understudy.matcher` | [`SpecificationCheck`](https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Internal/SpecificationCheck.php) | Says what the engine would say, before the test runs. | +| `understudy.matcherLeak` | [`MatcherLeakRule`](https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Rule/MatcherLeakRule.php) | A matcher written outside a specification. | +| `understudy.returns` | [`VoidReturnsRule`](https://github.com/rasuvaeff/understudy-phpstan/blob/55df82300b4eba83c101d835f6abe02159215d97/src/PhpStan/Rule/VoidReturnsRule.php) | `returns()` on a method that returns nothing. | Silence one by its identifier: diff --git a/docs/src/guide/doubles/creating.md b/docs/src/guide/doubles/creating.md index d3c03c8..7afaca2 100644 --- a/docs/src/guide/doubles/creating.md +++ b/docs/src/guide/doubles/creating.md @@ -53,7 +53,7 @@ What a class double does and does not do: | Public and protected methods | overridden and dispatched; a protected one shows up in the transcript and under strict mode, but PHP's own visibility keeps it out of a specification closure | | Private and static methods | untouched — the target keeps them, because there is no instance state to intercept | | The destructor | replaced with an empty one, so nothing is torn down that was never built | -| Writable public properties | start at an empty value of their type; object-typed, hooked, `final`, `readonly` and `private(set)` ones are left uninitialized, and reading one raises PHP's own error | +| Writable public properties | a declared default is kept; everything else starts at an empty value of its type — including a property **promoted** through the constructor, because the constructor is skipped, so `$double->promoted` differs from the real object. Object-typed, hooked, `final`, `readonly` and `private(set)` ones are left uninitialized, and reading one raises PHP's own error | | `clone` | produces a double of its own: same contracts, no expectations, no call log, owned by the context that cloned it | A `readonly` target produces a `readonly` double, which PHP requires and which diff --git a/docs/src/guide/failure-messages.md b/docs/src/guide/failure-messages.md index 545b0f4..8eda724 100644 --- a/docs/src/guide/failure-messages.md +++ b/docs/src/guide/failure-messages.md @@ -80,6 +80,13 @@ often exactly the one the test meant to write. When nothing at all is configured for the method, there is nothing to compare against and the message stays the single line naming it. +## What is redacted + +A parameter the contract marks `#[\SensitiveParameter]` is rendered as its type +and nothing else — `login('user', string SensitiveParameter)` — in every message +and in `transcript()`, the way PHP redacts such a parameter in its own traces. +Everything else is printed as written; see [Security](/guide/security). + ## Reading a failure as data An adapter or a reporter should not parse these strings. @@ -87,7 +94,8 @@ An adapter or a reporter should not parse these strings. `examples/structured-failures.php` in the repository. The wording of a message is not part of the public contract — a patch release -may reword one. What is frozen from v0.1.0 are the `FailureKind` cases and the -readonly fields of `VerificationFailure`, which is why anything acting on a -failure reads those instead. A test asserting on the exact text of a message is -asserting on prose. +may reword one. What is stable are the readonly fields of `VerificationFailure` +and every existing `FailureKind` case, which is why anything acting on a failure +reads those instead; a **new** kind may arrive in a minor release, so match on +the enum with a `default` arm rather than exhaustively. A test asserting on the +exact text of a message is asserting on prose. diff --git a/docs/src/guide/lifecycle/index.md b/docs/src/guide/lifecycle/index.md index 368843e..ad68c93 100644 --- a/docs/src/guide/lifecycle/index.md +++ b/docs/src/guide/lifecycle/index.md @@ -65,7 +65,11 @@ arithmetic reports `int<-1, max>` before the test even runs. ## The transcript `transcript()` renders every call and its outcome, and retains every invocation -until `reset()` or `checkpoint()`. +until `reset()`. `checkpoint()` clears only the calls it **settled** — the ones +a matching `expect()` or a successful `verify()` claimed — because an unclaimed +call is still what `nothingElse()` reads; a call covered by a `when()` stub +alone survives every checkpoint of a long test. Use `reset()`, `scope()` or +`lean()` to let go of everything. Avoid unbounded hot loops through a double when the arguments or results hold large object graphs; use a real fake for load-sized workloads. Where the diff --git a/docs/src/guide/modes.md b/docs/src/guide/modes.md index 5378443..ae2cc46 100644 --- a/docs/src/guide/modes.md +++ b/docs/src/guide/modes.md @@ -33,6 +33,14 @@ so `$a->b()->c()` says so rather than inventing a third collaborator the test never asked for. Registering a factory for `C` is how you say you meant it — see [Defaults registry](/guide/defaults). +A built-in interface is the exception: a method declared `: Stringable`, +`: Countable`, `: JsonSerializable`, `: ArrayAccess` or `: IteratorAggregate` +answers `NoDefaultValue` and names the way out, even though `Understudy::for()` +doubles all five. Such a return type almost always means a concrete +implementation, and standing a stub in for it would answer a question the test +did not ask. An interface of your own that extends one of them does get a nested +double. + Where no safe value exists, the double says so, and names the way out. ## Strict fails at the call diff --git a/docs/src/guide/security.md b/docs/src/guide/security.md index cdc1bbd..6168dc1 100644 --- a/docs/src/guide/security.md +++ b/docs/src/guide/security.md @@ -19,6 +19,15 @@ never keyed by `spl_object_id()`, which PHP reuses after collection. An id-keyed registry would hand one double's registrations to an unrelated object that happened to be allocated in the same slot. +**Arguments are printed verbatim in failure messages and in `transcript()`, with +one exception.** A parameter the contract marks `#[\SensitiveParameter]` is +rendered as its type and nothing else — `login('user', string +SensitiveParameter)` — the way PHP redacts such a parameter in its own stack +traces. Everything else goes into the message as written, and a failure message +is read from a CI log: mark the parameter, or keep the secret out of the +argument. The literals your own specification passes are not redacted; they are +in your test file already. + ::: danger It is a development dependency Do not install it in production. `composer require --dev`. ::: diff --git a/docs/src/guide/static-analysis.md b/docs/src/guide/static-analysis.md index 858c34c..5d46905 100644 --- a/docs/src/guide/static-analysis.md +++ b/docs/src/guide/static-analysis.md @@ -45,7 +45,8 @@ $repository->find(Arg::int()); // a real call, not a specification A matcher reaching a real call raises `MatcherLeaked` at run time, and an analyser package that hid it would be worse than no package at all. Both report -it — PHPStan under the identifier `understudy.matcherLeak`. +it — PHPStan under the identifier `understudy.matcherLeak`, at every level; +Psalm through its own `MixedArgument`, which exists only at `errorLevel="1"`. Everything else around a specification keeps its reports too: a wrong argument beside a matcher, a method the double does not have, the statements around the @@ -59,7 +60,7 @@ closure. | `understudy.cardinality` | `times(5, 2)`, a negative bound, `verify(…, never: true, times: 3)`, `times` beside a `minimum` | | `understudy.matcher` | a matcher whose kind the parameter can never accept: `Arg::int()` where a `string` is declared | | `understudy.returns` | `returns()` on a method declared `void`, where no value is ever observed | -| `understudy.matcherLeak` | a matcher written outside a specification, where it reaches the code as a value | +| `understudy.matcherLeak` | a matcher written outside a specification and outside any closure, where it reaches the code as a value; one hoisted into a variable, stored on a property or written in a closure handed over later is not one | Psalm reports the same family under one issue type, `UnderstudyMisuse`. diff --git a/docs/src/guide/stubbing/matchers.md b/docs/src/guide/stubbing/matchers.md index ee3e3df..47b26dc 100644 --- a/docs/src/guide/stubbing/matchers.md +++ b/docs/src/guide/stubbing/matchers.md @@ -47,16 +47,22 @@ other type would hide exactly the bug the declaration exists to prevent. ## A matcher that could never match is refused `Arg::int(min: 5, max: 1)` — and the same shape in `Arg::float()` and -`Arg::count()` — describes an empty range, and `Arg::string('/[unclosed')` is -not a pattern PCRE compiles. Both are typos, and both used to build a matcher -that answered "no" to every argument, so the mistake surfaced as an -expectation never met, with nothing pointing at its cause. They are refused -with `InvalidCallSpecification` where they are written. +`Arg::count()` — describes an empty range, `Arg::string('/[unclosed')` is not +a pattern PCRE compiles, and `Arg::instanceOf('Nope\Missing')` names a type +nothing can be an instance of. Each used to build a matcher that answered "no" +to every argument, so the mistake surfaced as an expectation never met, with +nothing pointing at its cause. They are refused with +`InvalidSpecificationArgument` where they are written. The broken pattern had a second cost: `preg_match()` raises a warning on every call, from inside the code under test — which is the one thing a matcher must never do. +A pattern that does compile is yours and is used as written, PCRE semantics +included: `$` matches before a trailing newline, so `Arg::string('/^ord-\d+$/')` +accepts `"ord-1\n"`. Anchor with `\z` (or add the `D` modifier) where that +matters. + ## `rest()` against `remaining()` They look similar and stand for different things: diff --git a/infection.json5 b/infection.json5 index 936a8f0..5e3509d 100644 --- a/infection.json5 +++ b/infection.json5 @@ -28,6 +28,16 @@ // which is filtered by `===` afterwards), and removing the call leaves // the value unindexed, which is always correct because the index is an // optimisation of a walk that still exists. + // + // Re-measured 2026-09-05 for 0.9.0, the 1.0 candidate: 3093 mutants, + // 182 escaped, Covered Code MSI 94.1% — three mutants above 94. The + // 1.0-readiness review asked for 94 "if the measurement after the + // changes is at or above it", and it is, by a margin smaller than the + // dozens by which the CI mutant count differs from the local one. A gate + // whose verdict flips on which PHP built the run measures the + // environment, not the suite (see AGENTS.md), so the number stays at 92 + // until the escaped list — TargetUnifier 45, FinalStripper 29, Runtime 28 + // — has been hunted down far enough to give 94 real headroom. "minMsi": 92, "mutators": { "@default": true, diff --git a/llms.txt b/llms.txt index bd345eb..27f40be 100644 --- a/llms.txt +++ b/llms.txt @@ -200,9 +200,10 @@ is not a type error in an IDE. `which()` calls only a public, non-static method needing no arguments; anything else, including a getter that throws, is a mismatch rather than an error — but a predicate handed to `satisfies()` is the test's own code, so an exception in it travels. A matcher that could never -match is refused at construction with `InvalidCallSpecification`: an inverted -range (`int`/`float`/`count`) and a pattern PCRE cannot compile. A matcher that -reaches a real call raises `MatcherLeaked`. +match is refused at construction with `InvalidSpecificationArgument`: an +inverted range (`int`/`float`/`count`), a pattern PCRE cannot compile, and +`instanceOf()` naming a type that is not loadable. A matcher that reaches a +real call raises `MatcherLeaked`. `Arg::captor(?class-string $class = null): Captor` builds a typed argument captor — the replacement for reading `args[N]` out of the call log: @@ -261,10 +262,6 @@ Pest declares its own global `expect()`. Import this one as `use function Rasuvaeff\Understudy\expect as expectCall;`, or call `Understudy::expect()`. -`Cardinality` is the immutable call-count value object. Its factories are -`exactly()`, `atLeast()`, `between()`, `any()`, and `never()`; `allows()` tests -a count and `describe()` renders it for diagnostics. - ### Chaining behaviour — `then()` ```php @@ -381,9 +378,10 @@ $calls = Understudy::calls(fn () => $repo->find(Arg::any())); // list $repo->find(Arg::any())); // ?Invocation ``` -`Invocation`: `->method`, `->args`, `->sequence`, `->didReturn()`, -`->didThrow()`, `->returned()`, `->thrown()`. Its outcome is held as an -`Outcome`, built by `Outcome::returnedValue()` / `Outcome::thrownError()`. +`Invocation`: `->method`, `->args`, `->sequence`, `->sensitiveArguments`, +`->argsAfter()`, `->didReturn()`, `->didThrow()`, `->returned()`, `->thrown()`, +`->callOriginal()`. The outcome is asked about, never inferred from a value: +`null` is a valid return. `returned()` on a call that threw raises `OutcomeUnavailable` — `null` is a valid return value and cannot signal "nothing". @@ -424,8 +422,8 @@ All implement `Rasuvaeff\Understudy\Exception\UnderstudyError`. | Exception | Raised when | |---|---| -| `InvalidCallSpecification` | the closure made no call, or threw first | -| `InvalidSpecificationArgument` | an argument no run could act on: `times(5, 2)`, a negative count, `returns()` with nothing to return, `Arg::instanceOf()` naming a class that is not loadable. Extends `\InvalidArgumentException`, which is what these paths threw before they implemented `UnderstudyError` | +| `InvalidCallSpecification` | the SHAPE of a specification is wrong: the closure made no call on a double, called a static method or threw first; a tail matcher is not the last argument; the specification stopped early without `Arg::rest()`; an empty `allOf()`/`anyOf()`; `verify()` arguments that contradict each other; a facade handed an object that is not a double; a protocol armed over a running one | +| `InvalidSpecificationArgument` | a VALUE no run could act on: `times(5, 2)`, a negative count, `returns()` with nothing to return, an inverted `Arg::int/float/count()` range, an `Arg::string()` pattern PCRE cannot compile, `Arg::instanceOf()` naming a type that is not loadable. Extends `\InvalidArgumentException` | | `UnsupportedTarget` | target missing, undoublable, or targets conflict | | `StrictModeViolation` | a strict double got an unconfigured call | | `NoDefaultValue` | loose mode has no safe value for the return type | @@ -467,9 +465,10 @@ try { } ``` -The readonly fields of `VerificationFailure`/`FailureKind` are frozen public -API from v0.1.0 — renaming, removing or retyping one is a major-version -change. New kinds and newly-populated fields are additive. +The readonly fields of `VerificationFailure` and the existing `FailureKind` +cases are stable: renaming, removing or retyping one is a major-version change. +A NEW kind, or a field newly populated for an existing kind, may arrive in a +minor — match on `FailureKind` with a `default` arm, never exhaustively. ## Not supported diff --git a/resources/skills/rasuvaeff-understudy/SKILL.md b/resources/skills/rasuvaeff-understudy/SKILL.md index 8120081..3e438ff 100644 --- a/resources/skills/rasuvaeff-understudy/SKILL.md +++ b/resources/skills/rasuvaeff-understudy/SKILL.md @@ -30,7 +30,7 @@ Two rules the whole API rests on: closure must contain exactly one direct call on a double; anything else raises `InvalidCallSpecification`. 2. **The double exposes nothing beyond its contract.** Every operation is a - static method on `Understudy` or one of the three free functions. There is + static method on `Understudy` or one of the four free functions. There is no `->shouldReceive()`, no `->expects()`, nothing to collide with a method the contract already has. @@ -85,6 +85,7 @@ not a refactor. **Reviewers: check for it every time.** | Did anything else happen to this double? | `Understudy::nothingElse($a, $b, …)` | | Both, for one double | `Understudy::allVerified($double)` | | Was the whole protocol exactly this, in order? | `Understudy::verifySequence(...)` | +| Fail at the call that breaks the order, not in teardown | `Understudy::expectSequence(...)`, armed before the run | | Was this double untouched? | `Understudy::unused($double)` | A call is *accounted for* when an `expect()` matched it or a **successful** @@ -98,8 +99,9 @@ is the tool when the whole protocol matters — it compares double identity too. ## Matchers — `Arg` `any`, `int(min:, max:)`, `float(min:, max:)`, `bool`, `string(matches:)`, -`same`, `not`, `instanceOf`, `satisfies`, `containing`, `count`, `which`, -`none`, `remaining`. +`same`, `not`, `allOf`, `anyOf`, `instanceOf`, `satisfies`, `containing`, +`count`, `which`, `none`, `remaining`, `rest`; a typed captor through +`Arg::captor()`. **Register broad first, specific after.** Matching takes the most recently registered stub that matches, so a catch-all registered last shadows @@ -116,7 +118,15 @@ does not hand over to an older stub. Every `Arg::*` returns `mixed`, so passing one where the contract says `int` is not an IDE type error. A matcher that reaches a real call raises -`MatcherLeaked`. +`MatcherLeaked`; one that could never match — an inverted range, a broken +pattern, an unloadable `instanceOf()` type — is refused where it is written +with `InvalidSpecificationArgument`. + +`Arg::rest()` is the one matcher that lets a specification stop before the +method's required parameters run out — `when(fn () => $s->record('svc', +Arg::rest()))`; stopping early without it is refused. `Arg::captor(X::class)` +plus `$captor->capture()` in the specification, then `$captor->last()` / +`all()`, is the typed replacement for reading `args[N]` out of the call log. ## Choosing a target @@ -127,6 +137,7 @@ is not an IDE type error. A matcher that reaches a real call raises | A non-final class | `Understudy::for(SomeClass::class)` | | A real instance, to spy on | `Understudy::for($real)` | | …and delegate unmatched calls to it | `Understudy::forwarding($double, $real)` | +| A real instance with unmatched calls delegated, in one expression | `Understudy::delegate(Contract::class, $real)` | | A `final` class | `Understudy::bypassFinals(X::class)` **before it loads** | Refused, each by name: a `final` class without bypass, a class with a @@ -153,8 +164,9 @@ Nearest registration wins by distance in the type graph; an equal-distance tie raises `AmbiguousDefaultFactory`. Registrations belong to the context and go with `reset()`. -Note: a **nullable** return answers `null` before the registry is consulted, -so a registration for `Book` has no effect on a method declared `?Book`. +A registration outranks `null` on a nullable return: a method declared `?Book` +answers with the registered `Book`, and is `null` only when nothing was +registered for it. ## Building the system under test @@ -177,7 +189,11 @@ Without one, call `Understudy::reset()` in your own teardown, always. `Understudy::scope(fn () => …)` opens a nested context verified on success and dropped either way. `Understudy::checkpoint()` verifies and clears what is -settled, keeping the doubles — for a test that runs in phases. +settled — the calls a matching `expect()` or a successful `verify()` claimed — +keeping the doubles, for a test that runs in phases. `Understudy::lean($double)` +keeps calls but not returned values: for a double whose returns own OS +resources, which the log would otherwise hold until the adapter's reset, after +your teardown. **Isolation is per Fiber; accounting is per test.** A Fiber gets its own recording phase, call log and sequence counter, but `verifyAll()`, `reset()`, @@ -207,7 +223,8 @@ A caught `VerificationFailed` carries `->failures(): list` `observedCalls`, `expectedCalls`, and its own rendered `summary`. The exception's message is exactly the summaries joined with a blank line. Use the records when a tool needs to act on a failure; use the message when a -human reads it. The fields are frozen public API from v0.1.0. +human reads it. The fields are stable; a new `FailureKind` case may arrive in a +minor, so match on the enum with a `default` arm. ## Migrating from Mockery or a hand-rolled spy diff --git a/src/Arg.php b/src/Arg.php index b5dc4b1..f78e93e 100644 --- a/src/Arg.php +++ b/src/Arg.php @@ -5,6 +5,7 @@ namespace Rasuvaeff\Understudy; use Rasuvaeff\Understudy\Exception\InvalidCallSpecification; +use Rasuvaeff\Understudy\Exception\InvalidSpecificationArgument; use Rasuvaeff\Understudy\Matcher\AllOf; use Rasuvaeff\Understudy\Matcher\AnyArgument; use Rasuvaeff\Understudy\Matcher\AnyOf; @@ -92,6 +93,9 @@ public static function string(?string $matches = null): mixed return new StringMatching($matches); } + /** + * Matches a `bool`, either value. + */ public static function bool(): mixed { return new BooleanValue(); @@ -240,7 +244,7 @@ public static function which(string $method, mixed $value): mixed private static function bounds(string $matcher, int|float|null $minimum, int|float|null $maximum): void { if ($minimum !== null && $maximum !== null && $maximum < $minimum) { - throw InvalidCallSpecification::invertedBounds($matcher, $minimum, $maximum); + throw InvalidSpecificationArgument::invertedBounds($matcher, $minimum, $maximum); } } @@ -274,7 +278,7 @@ private static function pattern(string $pattern): void restore_error_handler(); if (!$compiled) { - throw InvalidCallSpecification::invalidPattern($pattern, self::reason($reason)); + throw InvalidSpecificationArgument::invalidPattern($pattern, self::reason($reason)); } } diff --git a/src/Cardinality.php b/src/Cardinality.php index 3acd739..9036469 100644 --- a/src/Cardinality.php +++ b/src/Cardinality.php @@ -10,7 +10,12 @@ * How many times a call is allowed to happen. `null` as the maximum means no * upper bound. * - * @api + * `@internal` since 0.9: no public path accepts or returns one — `times()` and + * `verify()` take integers, and a `VerificationFailure` carries the bounds as + * `expectedMinimum`/`expectedMaximum`. Declaring it `@api` promised a value + * object nobody could hand to the library or get back from it. + * + * @internal */ final readonly class Cardinality { @@ -27,6 +32,9 @@ private function __construct( } } + /** + * Exactly this many calls — the default an `expect()` starts with. + */ public static function exactly(int $times): self { $bounded = self::nonNegative($times); @@ -34,11 +42,18 @@ public static function exactly(int $times): self return new self($bounded, $bounded); } + /** + * This many calls or more, with no upper bound. + */ public static function atLeast(int $minimum): self { return new self(self::nonNegative($minimum), null); } + /** + * A closed range, or an open one when the maximum is null — the shape + * `times($minimum, $maximum)` and `verify(minimum:, maximum:)` build. + */ public static function between(int $minimum, ?int $maximum): self { return new self( @@ -70,6 +85,9 @@ public static function any(): self return new self(0, null); } + /** + * Not even once — `verify(…, never: true)`. + */ public static function never(): self { return new self(0, 0); diff --git a/src/Exception/BypassUnavailable.php b/src/Exception/BypassUnavailable.php index 6ef4636..392b88c 100644 --- a/src/Exception/BypassUnavailable.php +++ b/src/Exception/BypassUnavailable.php @@ -38,6 +38,10 @@ public static function notAClass(string $class, string $kind): self )); } + /** + * Something else already transforms PHP source on `file://`; replacing it + * would silently disable whatever it does. + */ public static function foreignWrapper(string $owner): self { return new self(sprintf( diff --git a/src/Exception/ContextOwnershipViolation.php b/src/Exception/ContextOwnershipViolation.php index fc71027..57bc650 100644 --- a/src/Exception/ContextOwnershipViolation.php +++ b/src/Exception/ContextOwnershipViolation.php @@ -13,6 +13,10 @@ */ final class ContextOwnershipViolation extends \LogicException implements UnderstudyError { + /** + * A double was configured or verified from a context other than the one + * that created it — another Fiber, or outside the scope it was built in. + */ public static function forDouble(): self { return new self( diff --git a/src/Exception/ForgottenDouble.php b/src/Exception/ForgottenDouble.php index f6da06f..d9878db 100644 --- a/src/Exception/ForgottenDouble.php +++ b/src/Exception/ForgottenDouble.php @@ -53,6 +53,10 @@ public static function propertyAfterReset(string $property): self )); } + /** + * A facade — `strict()`, `verify()`, `nothingElse()`, `forget()` itself — + * was asked about a double that `Understudy::forget()` retired. + */ public static function retired(): self { return new self( diff --git a/src/Exception/InvalidCallSpecification.php b/src/Exception/InvalidCallSpecification.php index a7e3fa9..c588e08 100644 --- a/src/Exception/InvalidCallSpecification.php +++ b/src/Exception/InvalidCallSpecification.php @@ -5,15 +5,29 @@ namespace Rasuvaeff\Understudy\Exception; /** - * The closure handed to when()/verify()/calls() did not describe one call the - * way a specification must: no direct call on an understudy, more than one, or - * arguments that cannot form a valid specification — which includes a matcher - * configured so that it could never match anything. + * A specification of the wrong SHAPE: the closure handed to when()/verify()/ + * calls() made no direct call on an understudy, made it on a static method or + * threw first; a tail matcher was not the last argument; the specification + * stopped before the required parameters ran out without saying so with + * `Arg::rest()`; a combinator was given nothing to combine; `verify()` was + * handed arguments that contradict each other; a facade was handed an object + * that is not a double; a protocol was armed over one still running. + * + * The line between this class and {@see InvalidSpecificationArgument}: this + * one is about how a specification is written — what is called, where, how + * many times — and that one about a VALUE inside it that no run could act on + * (an inverted range, a count below zero, a pattern PCRE cannot compile, a + * type that is not loadable). A `LogicException`, because the test is + * malformed rather than given a bad number. * * @api */ final class InvalidCallSpecification extends \LogicException implements UnderstudyError { + /** + * The closure ran to its end without any generated method signalling: it + * called nothing on a double, or only something that is not one. + */ public static function noCallRecorded(): self { return new self( @@ -54,6 +68,10 @@ public static function neverBesideACount(string $bound): self )); } + /** + * `verify($call, times: 2, minimum: 1)`: an exact count leaves a bound + * nothing to constrain, so one of the two is a mistake. + */ public static function exactCountBesideABound(): self { return new self( @@ -91,43 +109,6 @@ public static function emptyCombinator(string $matcher): self )); } - /** - * A range matcher whose maximum sits below its minimum, so it describes an - * empty range and could never match. - * - * @param non-empty-string $matcher the factory that was called, without `Arg::` - * @param int|float $minimum the lower bound as it was given - * @param int|float $maximum the upper bound as it was given - */ - public static function invertedBounds(string $matcher, int|float $minimum, int|float $maximum): self - { - return new self(sprintf( - "`Arg::%s()` was given a minimum of %s and a maximum of %s, so it describes an empty " - . "range and would match no argument at all.\n" - . 'Order the bounds the other way, or leave one of them out to keep that side open.', - $matcher, - var_export($minimum, return: true), - var_export($maximum, return: true), - )); - } - - /** - * A pattern handed to `Arg::string()` that PCRE cannot compile. - * - * @param non-empty-string $pattern the pattern as it was written - * @param non-empty-string|null $reason what PCRE said, when it said anything - */ - public static function invalidPattern(string $pattern, ?string $reason): self - { - return new self(sprintf( - "`Arg::string()` was given `%s`, which is not a valid PCRE pattern%s\n" - . 'It would match no string and would raise a warning inside the code under test on ' - . 'every call. Check the delimiters and the escaping.', - $pattern, - $reason === null ? '.' : ': ' . $reason . '.', - )); - } - /** * @param non-empty-string $matcher * @param non-empty-string $operand @@ -143,6 +124,10 @@ public static function tailMatcherInCombinator(string $matcher, string $operand) )); } + /** + * `expectSequence()` with no steps: arming an empty protocol would put every + * later call on trial with nothing to try it against. + */ public static function emptySequence(): self { return new self( @@ -213,6 +198,10 @@ public static function omittedTailNeedsRest(string $method, string $matcher): se )); } + /** + * The specification closure threw before any generated method signalled; + * the original is kept as `previous`, because it is the actual mistake. + */ public static function closureFailed(\Throwable $previous): self { return new self( diff --git a/src/Exception/InvalidSpecificationArgument.php b/src/Exception/InvalidSpecificationArgument.php index 37fb50b..4eed37f 100644 --- a/src/Exception/InvalidSpecificationArgument.php +++ b/src/Exception/InvalidSpecificationArgument.php @@ -5,17 +5,19 @@ namespace Rasuvaeff\Understudy\Exception; /** - * An argument to a specification that no run could act on: a maximum call + * A VALUE inside a specification that no run could act on: a maximum call * count below its minimum, a negative count, `returns()` with nothing to - * return. + * return, a matcher range whose maximum sits below its minimum, a pattern + * PCRE cannot compile, `Arg::instanceOf()` naming a type that is not + * loadable. * - * Extends `\InvalidArgumentException` because that is what these three paths - * have always thrown, and a user's `catch (\InvalidArgumentException $e)` - * around them must keep working. It implements `UnderstudyError` because that - * interface says it is implemented by every exception this library throws, - * and these three were the exceptions to that — so a `catch (UnderstudyError - * $e)`, which the documentation recommends for catching misuse of Understudy - * itself, walked straight past them. + * The line between this class and {@see InvalidCallSpecification}: that one + * is about the SHAPE of a specification — what is called, where, how many + * times — and this one about a number, a pattern or a name given to it. It + * extends `\InvalidArgumentException` because that is the SPL type these + * paths have always been, and a user's `catch (\InvalidArgumentException $e)` + * around them keeps working; it implements `UnderstudyError` because that + * interface is implemented by every exception this library throws. * * @api */ @@ -70,4 +72,41 @@ public static function unknownType(string $type): self $type, )); } + + /** + * A range matcher whose maximum sits below its minimum, so it describes an + * empty range and could never match. + * + * @param non-empty-string $matcher the factory that was called, without `Arg::` + * @param int|float $minimum the lower bound as it was given + * @param int|float $maximum the upper bound as it was given + */ + public static function invertedBounds(string $matcher, int|float $minimum, int|float $maximum): self + { + return new self(sprintf( + "`Arg::%s()` was given a minimum of %s and a maximum of %s, so it describes an empty " + . "range and would match no argument at all.\n" + . 'Order the bounds the other way, or leave one of them out to keep that side open.', + $matcher, + var_export($minimum, return: true), + var_export($maximum, return: true), + )); + } + + /** + * A pattern handed to `Arg::string()` that PCRE cannot compile. + * + * @param non-empty-string $pattern the pattern as it was written + * @param non-empty-string|null $reason what PCRE said, when it said anything + */ + public static function invalidPattern(string $pattern, ?string $reason): self + { + return new self(sprintf( + "`Arg::string()` was given `%s`, which is not a valid PCRE pattern%s\n" + . 'It would match no string and would raise a warning inside the code under test on ' + . 'every call. Check the delimiters and the escaping.', + $pattern, + $reason === null ? '.' : ': ' . $reason . '.', + )); + } } diff --git a/src/Exception/UnsupportedTarget.php b/src/Exception/UnsupportedTarget.php index 464cceb..99102fe 100644 --- a/src/Exception/UnsupportedTarget.php +++ b/src/Exception/UnsupportedTarget.php @@ -12,6 +12,10 @@ */ final class UnsupportedTarget extends \LogicException implements UnderstudyError { + /** + * No class or interface of that name is loadable — usually a typo, or a + * class the autoloader cannot see from the test. + */ public static function missing(string $target): self { return new self(sprintf( diff --git a/src/Invocation.php b/src/Invocation.php index 8117616..01dd85d 100644 --- a/src/Invocation.php +++ b/src/Invocation.php @@ -45,6 +45,8 @@ final class Invocation * `#[\SensitiveParameter]`; carried on the call so a * failure message and a transcript can redact the value * the way PHP redacts it in its own traces + * + * @internal built by the dispatcher; the last three parameters are its bookkeeping */ public function __construct( public readonly string $method, @@ -113,6 +115,13 @@ public function callOriginal(): mixed return Runtime::callOriginal($this->double, $this->method, $this->liveArgs); } + /** + * The wrapped form of {@see recordReturned()} and {@see recordThrown()}, + * kept for the callers that still hand over an `Outcome`; dispatch itself + * uses the scalar recorders because every call reaches them. + * + * @internal + */ public function recordOutcome(Outcome $outcome): void { $this->outcome ??= $outcome; @@ -193,11 +202,18 @@ public function isAccounted(): bool return $this->accounted; } + /** + * Whether the call answered with a value — `null` included, which is why + * this is asked rather than inferred from {@see returned()}. + */ public function didReturn(): bool { return $this->returnedState ?? $this->outcome?->didReturn() ?? false; } + /** + * Whether the call ended in a throwable, which {@see thrown()} then holds. + */ public function didThrow(): bool { return $this->returnedState !== null @@ -205,6 +221,12 @@ public function didThrow(): bool : $this->outcome?->didThrow() ?? false; } + /** + * The value the call answered with. + * + * @throws OutcomeUnavailable when the call threw instead, or when the + * understudy is lean and did not keep the value + */ public function returned(): mixed { if ($this->returnedState === true) { @@ -224,6 +246,9 @@ public function returned(): mixed return $this->outcome?->returned($this->method); } + /** + * The throwable the call ended in, or null when it returned. + */ public function thrown(): ?\Throwable { return $this->returnedState !== null diff --git a/src/Outcome.php b/src/Outcome.php index 27225c8..0aed476 100644 --- a/src/Outcome.php +++ b/src/Outcome.php @@ -11,7 +11,13 @@ * because `null` is a perfectly valid return value and cannot double as * "nothing was returned". * - * @api + * `@internal` since 0.9: dispatch records outcomes through the scalar path + * (`Invocation::recordReturned()` / `recordThrown()`), so no public path ever + * builds or receives one — `Invocation::didReturn()`, `returned()`, `didThrow()` + * and `thrown()` are the public reading of an outcome. It was `@api` by + * inheritance from the first design, not because anything reached it. + * + * @internal */ final readonly class Outcome { diff --git a/src/Runtime/Runtime.php b/src/Runtime/Runtime.php index e637f68..944b3b2 100644 --- a/src/Runtime/Runtime.php +++ b/src/Runtime/Runtime.php @@ -7,7 +7,6 @@ use Rasuvaeff\Understudy\Codegen\DoubleFactory; use Rasuvaeff\Understudy\Defaults\TypeDefaultResolver; use Rasuvaeff\Understudy\Exception\ForgottenDouble; -use Rasuvaeff\Understudy\Exception\InvalidCallSpecification; use Rasuvaeff\Understudy\Exception\MatcherLeaked; use Rasuvaeff\Understudy\Exception\NeverMethodCalled; use Rasuvaeff\Understudy\Exception\OriginalCallUnavailable; @@ -343,9 +342,9 @@ public static function forget(object $double): void { $owner = self::ownerOf($double); - if ($owner === null || $owner->stateOf($double) === null) { - throw InvalidCallSpecification::noCallRecorded(); - } + // The facade has already answered for a stranger, a retired double and + // a foreign context through `stateOf()`; what reaches here is owned. + \assert($owner !== null && $owner->stateOf($double) !== null); $owner->forget($double); self::owners()->offsetUnset($double); diff --git a/src/Understudy.php b/src/Understudy.php index c57d3bb..33fe2b0 100644 --- a/src/Understudy.php +++ b/src/Understudy.php @@ -846,13 +846,12 @@ public static function unused(object $double): void */ public static function forget(object $double): void { - if (Runtime::ownerOf($double) === null) { - throw InvalidCallSpecification::notADouble('forget'); - } - - if (!Runtime::isOwnedByCurrentContext($double)) { - throw ContextOwnershipViolation::forDouble(); - } + // The same door every other facade goes through, so the answers are the + // same: a stranger is refused by name, a double retired or reset before + // answers `ForgottenDouble`, a foreign context answers + // `ContextOwnershipViolation`. Deciding by `ownerOf()` alone told the + // owner of a retired double that their object «is not one». + self::stateOf($double, 'forget'); Runtime::forget($double); } @@ -990,8 +989,6 @@ public static function allVerified(object $double): void * subject never reached, so arming one and never exercising it fails. * * @param callable(): mixed ...$calls - * - * @api */ public static function expectSequence(callable ...$calls): void { @@ -1220,8 +1217,13 @@ public static function scope(callable $callback, bool $strictStubs = false): mix } /** - * Verifies the current context and clears what has been settled, keeping - * the understudies themselves — for a long test that runs in phases. + * Verifies every context the test put understudies in and clears what has + * been settled — the claims a matching `expect()` or a successful + * `verify()` accounted for — keeping the understudies, their modes and + * their labels, for a long test that runs in phases. A call covered by a + * `when()` stub alone stays in the log, because `nothingElse()` still + * reads it; `reset()`, `scope()` and `lean()` are the ways to let go of + * everything. */ public static function checkpoint(bool $strictStubs = false): void { diff --git a/src/VerificationFailure.php b/src/VerificationFailure.php index da9f806..cccdc6f 100644 --- a/src/VerificationFailure.php +++ b/src/VerificationFailure.php @@ -23,10 +23,11 @@ * | `UnaccountedCalls` | label | — | — | unaccounted calls | the unaccounted calls | — | * | `UnusedDouble` | label | — | `0`/`0` | calls received | every call | — | * - * The readonly fields of this class, of {@see FailureKind}, and of the - * exceptions carrying them are frozen public API from v0.1.0: renaming, - * removing or retyping any of them is a major-version change. New kinds and - * newly-populated fields are additive and may arrive in a minor. + * The readonly fields of this class and every existing {@see FailureKind} + * case are stable: renaming, removing or retyping one is a major-version + * change. A NEW kind, or a field newly populated for an existing kind, may + * arrive in a minor — so match on `FailureKind` with a `default` arm; an + * exhaustive `match` over the enum is not a supported way to consume it. * * @api */ diff --git a/src/WhenBuilder.php b/src/WhenBuilder.php index a8d267b..83166fe 100644 --- a/src/WhenBuilder.php +++ b/src/WhenBuilder.php @@ -21,10 +21,13 @@ * * Not `final`, and the one class here that is not: `ExpectBuilder` extends it * to add the cardinality verbs, and the two are one fluent vocabulary rather - * than two. The cost is that `protected readonly Expectation` — an - * `@internal` type — is reachable from a subclass a consumer could write. That - * is tolerated rather than intended: nothing in the contract invites it, and - * closing it would mean duplicating every action verb. + * than two. For everyone else it is closed by contract, not by keyword: + * subclassing is not supported, the `protected readonly Expectation` it + * carries is an `@internal` type that may change in any release, and a subclass + * reading it is on its own. The keyword is missing only because closing the + * class would mean duplicating every action verb on `ExpectBuilder` — and a + * `@final` tag is not an option either, since Psalm would then refuse + * `ExpectBuilder` itself. * * @template TReturn * @@ -72,6 +75,10 @@ public function returns(mixed ...$values): static return $this; } + /** + * Throws this exact instance on the call — the same object every time the + * link answers, which is what a test holding a reference to it expects. + */ public function throws(\Throwable $error): static { $this->expectation->setAction(new ThrowError($error), $this->slot); diff --git a/src/functions.php b/src/functions.php index e51698e..4718111 100644 --- a/src/functions.php +++ b/src/functions.php @@ -77,6 +77,7 @@ function expectSequence(callable ...$calls): void * @param int<0, max>|null $times * @param int<0, max>|null $minimum * @param int<0, max>|null $maximum + * @param bool $never the call must not have happened at all; refused beside a count * * @api */ diff --git a/tests/ArgTest.php b/tests/ArgTest.php index d03ca7e..9d50c72 100644 --- a/tests/ArgTest.php +++ b/tests/ArgTest.php @@ -6,6 +6,7 @@ use Rasuvaeff\Understudy\Arg; use Rasuvaeff\Understudy\Exception\InvalidCallSpecification; +use Rasuvaeff\Understudy\Exception\InvalidSpecificationArgument; use Rasuvaeff\Understudy\Matcher\AllOf; use Rasuvaeff\Understudy\Matcher\AnyArgument; use Rasuvaeff\Understudy\Matcher\AnyOf; @@ -40,6 +41,7 @@ #[Test] #[Covers(Arg::class)] #[Covers(InvalidCallSpecification::class)] +#[Covers(InvalidSpecificationArgument::class)] #[Covers(AllOf::class)] #[Covers(AnyArgument::class)] #[Covers(AnyOf::class)] @@ -364,7 +366,7 @@ public static function value(): string #[DataProvider('invertedBoundsProvider')] public function anInvertedRangeIsRefusedWhereItIsWritten(callable $build, string $message): void { - Expect::exception(InvalidCallSpecification::class)->withMessage($message); + Expect::exception(InvalidSpecificationArgument::class)->withMessage($message); $build(); } @@ -430,11 +432,11 @@ public function anInvalidPatternIsRefusedWithPcresOwnReason(): void try { Arg::string('/[unclosed/'); - } catch (InvalidCallSpecification $refusal) { + } catch (InvalidSpecificationArgument $refusal) { $caught = $refusal; } - \assert($caught instanceof InvalidCallSpecification); + \assert($caught instanceof InvalidSpecificationArgument); Assert::string($caught->getMessage()) ->contains('`Arg::string()` was given `/[unclosed/`, which is not a valid PCRE pattern') ->contains('missing terminating ] for character class') @@ -454,7 +456,7 @@ public function refusingAPatternRaisesNoWarningOfItsOwn(): void try { Arg::string('/[unclosed/'); - } catch (InvalidCallSpecification) { + } catch (InvalidSpecificationArgument) { // The refusal is the subject of the test above; here only the // error channel is on trial. } @@ -474,7 +476,7 @@ public function refusingAPatternRestoresTheErrorHandler(): void try { Arg::string('/[unclosed/'); - } catch (InvalidCallSpecification) { + } catch (InvalidSpecificationArgument) { // Same as above: the refusal itself is tested elsewhere. } @@ -496,7 +498,7 @@ public function refusingAPatternRestoresTheErrorHandler(): void #[DataProvider('invalidPatternMessageProvider')] public function theRefusalSpellsOutWhatWasGiven(?string $reason, string $expected): void { - Assert::same(InvalidCallSpecification::invalidPattern('/[unclosed/', $reason)->getMessage(), $expected); + Assert::same(InvalidSpecificationArgument::invalidPattern('/[unclosed/', $reason)->getMessage(), $expected); } public static function invalidPatternMessageProvider(): iterable diff --git a/tests/Defaults/TypeDefaultResolverTest.php b/tests/Defaults/TypeDefaultResolverTest.php index a9cc1f6..752ae44 100644 --- a/tests/Defaults/TypeDefaultResolverTest.php +++ b/tests/Defaults/TypeDefaultResolverTest.php @@ -121,8 +121,8 @@ public function staticAnswersWithTheDoubleItself(): void $this->signature('static'), 'method', new RuntimeContext(), - false, - $double, + nested: false, + double: $double, ), $double, ); diff --git a/tests/ErrorPathTest.php b/tests/ErrorPathTest.php index 57b7c7a..9bb230c 100644 --- a/tests/ErrorPathTest.php +++ b/tests/ErrorPathTest.php @@ -425,7 +425,9 @@ public function aResourceParameterIsRefusedWithTheReasonItCannotBeDecided(): voi Assert::string($failure->getMessage()) ->contains('a resource cannot be passed as a constructor argument'); } finally { - \is_resource($handle) && fclose($handle); + if (\is_resource($handle)) { + fclose($handle); + } } } diff --git a/tests/LastCallAndForgetTest.php b/tests/LastCallAndForgetTest.php index f3431e8..0bb6de4 100644 --- a/tests/LastCallAndForgetTest.php +++ b/tests/LastCallAndForgetTest.php @@ -159,4 +159,37 @@ public function forgettingANonDoubleIsRefusedByName(): void Understudy::forget(new \stdClass()); } + + /** + * The object IS a double, so the answer is the one every other facade + * gives for a retired one — not «this object is not one», which sent the + * reader looking for a mistake they had not made. + */ + public function forgettingADoubleTwiceSaysItWasRetired(): void + { + $repository = Understudy::for(BookRepository::class); + Understudy::forget($repository); + + Expect::exception(ForgottenDouble::class)->withMessage( + 'This understudy was retired with Understudy::forget() and can no longer be asked ' + . 'anything — not calls, not verification. A replacement built afterwards is a ' + . 'different object; ask that one.', + ); + + Understudy::forget($repository); + } + + public function forgettingADoubleAfterResetSaysItIsGone(): void + { + $repository = Understudy::for(BookRepository::class); + Understudy::reset(); + + Expect::exception(ForgottenDouble::class)->withMessage( + "This understudy is no longer known to Understudy, but `forget()` was called on it.\n" + . 'It was created before a reset(); create doubles inside the test that uses them ' + . 'rather than sharing one across tests.', + ); + + Understudy::forget($repository); + } }