Skip to content

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 09:14
· 9 commits to master since this release
53afa55

The wave the 1.0 candidate turned out to still need: ten defects found by
migrating the monorepo onto the library and by probing the engine against its
own documentation. Two of them change what a specification means, which is why
this is a minor and not the tag after 0.9.0.

A specification is read the way the contract reads a call

  • An optional parameter a specification did not spell no longer arrives as
    the contract's default value.
    It used to, and arity therefore became an
    implicit part of every specification:
    claimReady(Arg::any(), Arg::any(), Arg::any()) did not match
    claimReady($now, 3, [], 100) on a method whose fourth parameter defaults to
    1000, and the report said never called beside a call whose only
    difference was a position the author never wrote. Every generated parameter
    now defaults to the arity sentinel, optional ones included, and what an
    omission means follows the contract: one it declares optional may be left
    out by any caller, so a specification that leaves it out says nothing about
    it and matches whatever was passed there; one it declares required is
    present in every real call, so stopping before it still has to be said with
    Arg::rest(). A failure message renders an unspelled position as , which
    is what tells it apart from an any() the test did write. Found migrating
    yii3-outbox and yii3-centrifugo (#123).
  • Arg::rest() works where the remaining parameters are optional. The
    matcher list called it "declared parameters left unspelled" and the engine
    refused it — translate(Arg::rest()) on a signature with four optional
    parameters raised "rest() … may only be the last argument" about argument
    #1 — because the optional ones had already become literals by the time the
    check looked. Same root cause, and the docs, the message and the matcher now
    agree (#125).
  • A real call is unchanged: dispatch materializes the declared default for
    an argument the caller omitted, so tag('alpha') and tag('alpha', 1) are
    still one call in the log. What changed with it is that a double's
    generated signature no longer advertises the contract's defaults — it
    carries the sentinel — so Reflection over a double reports them differently
    than Reflection over the contract.
  • mixed $v = null was a fatal error. The nullability widening applied to
    mixed too, and mixed|null is a type PHP refuses at compile time: an
    uncatchable fatal out of eval(), for a signature that is neither exotic nor
    rare. The widening now belongs to the branch where the union is real, and is
    driven by what the contract declares rather than by what the double renders.

Refusals for what used to be silent

  • A captor inside Arg::allOf(), anyOf(), not() or containing()
    matched and recorded nothing.
    The specification behaved correctly in every
    observable way except the one it was written for, and the only way to find
    out was an assertion on an empty captor further down the test. It is refused
    where it is written (#124).
  • A matcher inside a plain array argument matched nothing
    find(['id' => Arg::any()]) compares by identity — and said nothing about
    it. Refused, and Arg::containing() now reads matchers in its own entries,
    nested, so there is something to be refused towards.
  • Understudy's own refusals are no longer rewrapped in "the specification
    closure threw before it reached an understudy", which buried the sentence
    that said what to change.
  • A by-reference slot was chosen from arguments dispatch had not yet
    completed.
    referenceSlot() asks which expectation will answer before
    dispatch, and asked with the omitted arguments still sentinels: a
    specification spelling the contract's default answered "nothing configured",
    so the slot kept what the test had written through the reference instead of
    being replaced by the configured value. The call answered correctly and the
    next read did not.
  • A protocol step due on another double says so. Two doubles under one
    expectSequence() render every step by its call alone, so count() arriving
    on the wrong one read as the step that was due — identical text, and no hint
    that the difference was the receiver.

Additions

  • Understudy::strict() and Understudy::label() answer with the double
    they configured
    , so the mode can be chosen where the double is handed over:
    ClientInterface::class => Understudy::strict(Understudy::for(ClientInterface::class))
    used to store null and fail three steps away from the cause (#126).

  • WhenBuilder::throwsWith() builds the exception from the call it
    answers, one per call — the shape throws() cannot express, and which
    everyone re-derived as a throwing answers() closure (#127).

  • Invocation::arg() reads one argument by position or by the contract's
    own parameter name, and refuses a name the method does not declare rather
    than answering null (#127).

  • The API reference documents the satellites at their current versions.
    docs/.api-workspace/composer.json pinned understudy-psalm ^0.2,
    understudy-phpstan ^0.2, understudy-phpunit ^0.1 and understudy-testo ^0.1 — a caret on 0.x pins a minor — so the site reflected the plugins at
    0.2.0 and the adapters at 0.1.x while 0.8.0, 0.5.0, 0.2.0 and 0.3.0 were out,
    and the weekly rebuild rebuilt the same thing. The pins move to the current
    lines, the workspace lock is committed so the pages in the repository and on
    the site come from the same install, and docs.yml runs composer outdated --direct --strict in the workspace: a satellite release the pins do not
    reach is a red build now, not a stale page.

  • static-analysis.yml runs composer rector in the Psalm job. It is part of
    composer release-check and was run by nobody but a human before a tag —
    this package went out four times with it red under green builds.

  • Two defects of the reference generator that the refresh exposed:
    docs/scripts/reflect-rules.php took every string literal starting with
    understudy. for a rule identifier, so ClosureShape's php-parser attribute
    understudy.receiverOfCall sat on the rules page as a sixth identifier nobody
    could write into ignoreErrors (only constants named *IDENTIFIER count
    now); and generate-api.mjs had no label for a trait, so the PHPUnit
    adapter's page was headed «undefined».