feat(rector): PHPUnit↔Testo typed-assert & Repeat/Retry conversions, Assert API gaps (#270) - #292
Merged
Merged
Conversation
numeric() was a throwing stub; it now validates int, float and numeric strings (the string is normalised to a real number so the NumericTrait comparisons run against a number, not via PHP coercion). notBlank() is the inverse of blank(), keeping the same stance that false/0/'0' are valid data. ArrayType::sameElementsAs() canonicalizes both sides (recursive sort, keys discarded) and compares loosely, matching PHPUnit's assertEqualsCanonicalizing — these unblock the matching Rector conversions. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssertions and Repeat/Retry feat(rector): map PHPUnit Repeat/Retry attributes to Testo and back TypedAssertCallToTestoRector is the sibling of AssertCallToTestoRector for the assertions whose faithful Testo form is a typed head + matcher rather than a flat facade call: comparisons to Assert::numeric()->greaterThan() etc., array keys to Assert::array()->hasKeys()/doesNotHaveKeys(), assertEqualsCanonicalizing to sameElementsAs. assertEmpty/assertNotEmpty map to blank()/notBlank() only for an array subject, where blank()'s and empty()'s treatment of false/0/'0' coincide; other subjects are left untouched. The array-key matchers are variadic with no message parameter, so a trailing PHPUnit message is dropped there. TypedAssertChainRector gains the reverse sameElementsAs to assertEqualsCanonicalizing. PHPUnit 13.3 added #[Repeat]/#[Retry], so both directions convert now: RepeatRetryToTestoRector (PHPUnit to Testo) and the previously-stubbed RepeatRetryRector (Testo to PHPUnit, method-level only). PHPUnit's failureThreshold (aborting count, default 1) maps to Testo's maxFailures (tolerated count, default 0) off by one; markFlaky and class/function-level targets have no PHPUnit counterpart and are dropped. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Since PHPUnit 13.3 defect ordering requires test-run-history recording, and Infection disables that for its initial coverage run (it injects cacheResult="false"). With executionOrder="depends,defects" the runner then orders by nothing and executes zero tests, so Infection collects empty coverage and aborts with "No source code was executed" — failing infect:phpunit:ci. Ordering by depends alone keeps the mirror running the full suite on any PHPUnit 13.x. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…vailable TypedAssertChainRector always decomposed a chain into `$this->assert*` statements, but a chain inside a static method, a static closure or a data provider has no $this — the emitted calls then fatal with "using $this when not in object context". Mirror AssertCallToPhpUnitRector: emit `self::assert*` when the scope has no bound $this, `$this->assert*` otherwise. Surfaced by the PHPUnit mutation mirror, where a static assertion closure was mistranslated. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s in the PHPUnit mirror fix(test): recreate empty stub dirs in the PHPUnit mirror The mutation mirror only copied .php files, so a support directory read by relative path never reached tests/PhpUnit: an empty suite-location stub kept alive by a .gitkeep (EmptyRunTest), a VCR cassette dir and Rector .php.inc fixtures were all missing, so those tests errored. build-phpunit.php now mirrors any non-PHP support file under a Stub/Fixture/fixtures dir verbatim next to the relocated tests, deriving each plugin/bridge root's mirror base from a sample test file's namespace. SkipUnconvertibleTestMethodRector now also skips a test whose body still contains a Testo\Expect:: call after conversion — the substring matcher withMessageContaining has no PHPUnit counterpart and aborts the chain, leaving a runtime expectation that would fatal with StateNotFound under PHPUnit. These surfaced once the executionOrder fix let the mirror actually run its tests. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h test method test(rector): use a genuinely unrelated attribute in the RepeatRetryToTesto no-op fixture PHPUnit's #[Repeat]/#[Retry] are TARGET_METHOD only, so RepeatRetryRector now pushes a class-level Testo attribute down onto every test method (mirroring how Testo applies it to each test in the class) and removes it from the class, instead of leaving it untouched. A method carrying its own attribute of the same kind keeps it — method-level overrides the class default and is not doubled. Test methods are identified as in TestClassToTestCaseRector: the #[Test]-marked methods, or under a class-level #[\Testo\Test] every public non-static void/never non-lifecycle method. The no-op fixture for the reverse direction swapped PHPUnit's #[Group] (which the full set does convert, via GroupToTestoRector) for #[Small], which has no Testo counterpart, so it actually demonstrates an unconverted attribute. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… skip only truly unconvertible Expect SkipUnconvertibleTestMethodRector blanked a skipped method's whole body and stripped its data attributes. It now PREPENDS the markTestSkipped() and keeps the original body and attributes, so the unconvertible test stays visible and just reports as skipped — the skip throws before the kept (dead) body runs. Parameters are dropped only when no data source will feed them, so a data-driven test keeps its parameters and its dataset arity still matches (no more "dataset has more arguments than the method accepts" warnings). Lifecycle hooks are no longer emptied — they run their real body around a skipped test harmlessly now that the build mirrors the stub files a former-constructor #[Before] hook reads. The skip trigger for exception expectations is also made precise: instead of skipping on any leftover Testo\Expect:: call (which, depending on rule order, wrongly caught chains that ExpectExceptionToPhpUnitRector converts fine), it skips only a chain carrying a modifier with no PHPUnit form — anything other than withMessage/withCode/withMessagePattern. That recovers ~24 previously over-skipped tests, which now run and add mutation coverage. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was changed
Closes the #270 gap set (comparison / array-key / emptiness assertions and
assertEqualsCanonicalizing) and adds the brand-new PHPUnit 13.3#[Repeat]/#[Retry]attributes to the Rector bridge in both directions.Assert API (unblocks the conversions):
Assert::numeric()is now implemented (int / float / numeric-string, with the string normalised to a real number for the comparison matchers) instead of a throwing stub;Assert::notBlank()is added as the inverse ofblank()(keeping the stance thatfalse/0/'0'are valid data);ArrayType::sameElementsAs()is added — order-insensitive, canonicalizing comparison matching PHPUnit'sassertEqualsCanonicalizing. Each ships with self-test coverage.Rector — PHPUnit → Testo: new
TypedAssertCallToTestoRectorconverts the assertions whose faithful Testo form is a typed head + matcher — comparisons (assertGreaterThan→Assert::numeric()->greaterThan(), …), array keys (assertArrayHasKey/assertArrayNotHasKey→Assert::array()->hasKeys()/->doesNotHaveKeys()),assertEqualsCanonicalizing→sameElementsAs, andassertEmpty/assertNotEmpty→blank()/notBlank()only for an array subject (where the two notions of emptiness provably coincide; other subjects are left untouched). The variadic array-key matchers have no message parameter, so a trailing PHPUnit message is dropped there — documented.Rector — Repeat/Retry (both directions):
RepeatRetryToTestoRector(PHPUnit → Testo) and the previously-stubbedRepeatRetryRector(Testo → PHPUnit, method-level only) now convert#[Repeat]/#[Retry]. PHPUnit'sfailureThreshold(aborting count, default 1) maps to Testo'smaxFailures(tolerated count, default 0) off by one;markFlakyand class/function-level targets have no PHPUnit counterpart and are dropped.TypedAssertChainRectorgains the reversesameElementsAs→assertEqualsCanonicalizing.Mutation testing (the PHPUnit mirror): several fixes so
infect:phpunit:ciruns green again — it had been failing. (1)phpunit.xmldroppeddefectsfromexecutionOrder: since PHPUnit 13.3, defect ordering requires test-run-history recording, which Infection disables for its initial coverage run, so the runner executed zero tests and Infection aborted withNo source code was executed. (2)TypedAssertChainRectornow emitsself::assert*where$thisis unavailable (a static assertion closure was mistranslated to$this->assert*and fatally errored). (3)bin/build-phpunit.phpnow mirrors non-PHP support data (an empty stub dir kept by a.gitkeep, VCR cassettes, Rector*.php.incfixtures) next to the relocated tests, so tests that read fixtures by relative path work. (4)SkipUnconvertibleTestMethodRectornow skips a test still callingTesto\Expect::after conversion (the substring matcherwithMessageContaininghas no PHPUnit form). Verified locally: the mirror suite is green and Infection completes its initial run and proceeds to mutation.Docs updated:
bridge/rector/FEATURE_PARITY.md, bothTODO.mdfiles, and thetesto-write-tests/testo-migrate-from-phpunitskills.See commit history for details.
Why?
A real PHPUnit → Testo migration left 18 assertion call sites unconverted (issue #270), and PHPUnit 13.3 introduced native repeat/retry attributes that now have a faithful Testo counterpart. Updating PHPUnit in
tools/to 13.3.1 surfaced the mutation-testing CI failure, whose primary cause and the conversion gaps it unmasked are fixed here.Checklist
assertEqualsCanonicalizingcounterpart #270