feat: add Rector bridge for Testo/PHPUnit/Pest test conversion#248
Merged
Conversation
…ixture harness Introduces the `testo/bridge-rector` package: Rector rules to convert test suites between PEST, PHPUnit and Testo, starting with the Testo->PHPUnit direction. - `TestoToPhpunit\AssertCallToPhpUnitRector`: rewrites `Testo\Assert::*` static calls into PHPUnit `$this->assert*` calls, correcting the actual/expected argument order (Testo is actual-first, PHPUnit expected-first). - `Testing\` harness -- "inline tests for rules": a rule carries `#[TestRectorFixtures(...)]` pointing at co-located `*.php.inc` fixtures; a finder discovers such rules and an interceptor fans the fixtures into one data set each, asserting via a freshly-booted, per-rule Rector container built from Rector's own framework-agnostic services. Public surface = the attribute plus `RectorTestingPlugin`; the rest lives under `Testing\Internal`. - Fixtures (`*.php.inc`) and the dev-only suite wiring are `export-ignore`d; the reusable harness ships so downstream rule authors can use it (testo/* are require-dev + suggest). Wires the package into the root config: path repo, require-dev, and a `Bridge/Rector` suite scanning the rule sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The discovery finder matched every file under a scanned directory and then eagerly reflected each class it found. Non-source files that happen to live under the path — e.g. Rector `*.php.inc` fixtures that declare classes — were tokenized and reflected, which aborted discovery for the whole suite. Restrict the finder to `*.php` so only real PHP source is considered. This also unblocks co-locating `*.php.inc` fixtures next to the rules they test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to rules Builds out testo/bridge-rector across all three directions, with a feature-parity matrix (FEATURE_PARITY.md) and per-direction TODO/stub docs. Every rule ships co-located `*.php.inc` fixtures exercised by the inline test harness. Testo -> PHPUnit: assertion calls, typed-chain decomposition (Assert::int()->... into separate assert* lines, 1->N where needed), Expect::exception (bare), throw SkipTest -> markTestSkipped, #[Covers] -> #[CoversClass], lifecycle attributes, #[Group] (variadic -> repeated) plus class-level inheritance flattening via reflection, #[ExpectNoAssertions] -> #[DoesNotPerformAssertions]. PHPUnit -> Testo: assertion calls (actual/expected order restored), markTestSkipped -> throw SkipTest, #[CoversClass] -> #[Covers], setUp/tearDown -> lifecycle attributes, expectException (bare), both @dataProvider and #[DataProvider] -> #[\Testo\Data\DataProvider] (no phpunit dependency), #[Group] (annotation and attribute collapsed into one variadic #[\Testo\Filter\Group]), #[DoesNotPerformAssertions] -> #[ExpectNoAssertions]. Pest -> Testo: expect()->toX() -> Assert::*. The functional->class restructuring (test()/it() -> methods, hooks, datasets) is out of Rector's reach and shipped as documented stubs. Conversions with no faithful target (PHPUnit mocks/constraints/incomplete, Testo memory-leak/retry/repeat/cancel, Pest arch()) are documented stub rules, not silently dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… rule workaround The inline fixture harness reuses one Rector container across all of a rule's fixtures, but the source locator caches the aggregate built from the first file it sees (Rector only rebuilds it per file under PHPUnit). So reflection saw only the first fixture's classes, and reflection-based rules had to compensate. `RectorRunner` now resets the source locator before each fixture, so reflection — and the node scope derived from it — sees that fixture's classes. With that, `GroupInheritanceToPhpUnitRector` drops its self-compensating reset/restore of the locator and resolves the leaf class via the normal node scope (`ReflectionResolver::resolveClassReflection`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ators
`e4707ac` hardcoded `->name('*.php')` in the core `Tokenizer\Finder`, baking
the "only PHP source" assumption into discovery itself. Move that decision to
where it belongs: each `FileLocatorInterceptor` now claims only `*.php` files
it could actually `include` and reflect, and defers anything else (e.g.
`*.php.inc` Rector fixtures, which declare classes but are not loadable PHP).
- Revert the Finder hardcode — it matches every file again.
- Gate `locateFile()` on `extension() === 'php'` in InlineFinder, BenchFinder,
TestoAttributesLocatorInterceptor, NamingConventionLocator (also closes a
`SomethingTest.inc` suffix hole) and the bridge's RectorFixtureFinder
(required: otherwise its suite would load its own fixtures).
Deferring rather than hard-vetoing keeps the result independent of interceptor
order. Bridge/Rector 83/83, full suite unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the Rector bridge into the same release-please + subtree-split pipeline as the other bridges/plugins, and add the dev-only CI it ships to its split repo. The dedicated repository php-testo/bridge-rector is created out of band; on the first `bridge-rector-*` release tag the split action populates it. - release-please: add the bridge/rector package (testo/bridge-rector, component bridge-rector) and seed resources/version.json at 0.1.0. - split-publish: trigger on `bridge-rector-[0-9]*` tags. - bridge/rector: add CHANGELOG.md (release-please target) and the close-foreign-prs workflow; export-ignore /.github from composer archive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-path flow Rework the migration skill from a flat translation table into a phased orchestrator that routes to one of two detailed approach references, with deterministic automation scripts and verbatim subagent templates — modelled on testo-mutation-testing / testo-increase-coverage. - SKILL.md: 5 phases — restore point (BLOCKING), scope, tooling detection + approach choice, execution, verify & retire. Forces parallel subagents per file (independent work), contrasting the sequential sibling skills. - references/phpunit-to-testo-map.md: authoritative construct map + pitfalls, split into "Rector handles" vs "needs AI/human" columns. - references/migrate-with-rector.md (Approach A) and migrate-with-agents.md (Approach B): per-stage install/config/run/verify with gates and troubleshooting. testo.php is generated with `vendor/bin/testo init` before the subagent pass. - references/subagent-port-prompt.md: fixed per-file porting template. - scripts/: precheck.php (tooling + test-surface survey), scaffold-rector-config.php (generate a Rector config wired to the bridge set), scan-residuals.php (rank files into batched work-lists). - README.md: refresh the skill's one-liner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # composer.json # resources/version.json
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Close every 🧩 item in bridge/rector/FEATURE_PARITY.md; what remains is now either ✅, a 🟡 with a documented residual, or an intentional ⛔. Bridge/Rector suite 83 → 107. - Data providers (Testo → PHPUnit): new DataProviderToPhpUnitRector renames #[Data\DataProvider] → #[Attributes\DataProvider] and #[Data\DataSet([...],'l')] → #[Attributes\TestWith([...],'l')], preserving args verbatim. - Exception fluent (both directions): ExpectExceptionToPhpUnitRector expands the Testo chain into separate $this->expect* statements (Node[] return); ExpectExceptionToTestoRector folds an uninterrupted run of sibling expectExceptionMessage/Code calls into the ->withMessage()/->withCode() chain. Residual: withMessageContaining is left untouched (substring vs PCRE). - Method-level group inheritance (Testo → PHPUnit): GroupInheritanceToPhpUnitRector now also walks each leaf method's prototype chain and flattens the parent method's groups onto the leaf, idempotently. Traits are excluded at the method level to match Testo's own getPrototype()-based resolution. - Structural (both directions): promote ExtendsTestCaseToTestoRector (remove `extends TestCase`, make discovery attribute-based via #[\Testo\Test]) and TestClassToTestCaseRector (the mirror) from stubs. Residual: methods are not renamed; scope is direct-TestCase / no-existing-base only. Each rule ships co-located *.php.inc fixtures and is registered in its direction set; TODO.md files updated accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eatures When a new user-facing feature lands (attribute, assertion, lifecycle hook, exception, …), it almost always has a PHPUnit/Pest equivalent, so the testo/bridge-rector conversion rules should gain a counterpart (+ fixtures) or a documented stub. Mirrors the existing "update the skills" contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pest -> Testo was previously declared intractable because Rector cannot
synthesize a host class for file-level test() closures. Targeting Testo's
function-based discovery instead of class methods dissolves that blocker:
each test()/it() becomes a #[Test] free function, no class required.
TestCallToFunctionRector (STMTS_AWARE, for collision-free naming):
- test('desc')/it('desc') -> #[\Testo\Test] function; name is the declarator
prefix + snake(description) (test_/it_), description kept as the docblock;
in-file collisions get a _2/_3 suffix.
- beforeEach/afterEach/beforeAll/afterAll -> functions carrying
Testo\Lifecycle\{BeforeTest,AfterTest,BeforeClass,AfterClass}.
- folds the fluent chain in the same pass: ->group -> #[Group],
->covers -> #[Covers], ->throws(X[,msg]) -> Expect::exception()+never,
->skip('r') -> throw SkipTest('r'), ->with([rows]) -> repeated #[DataSet].
- bails (leaves the statement untouched) on a non-literal description, a
use(...)-capturing closure, or any unrecognised modifier.
Synthesized Function_ nodes set ->namespacedName explicitly; otherwise
Rector's scope refresher fatals and silently rolls back the change.
Collapse the six now-subsumed stubs (test-function/lifecycle/throws/skip/
group/dataset) into this single rule; keep UsesToTraitRector and
ArchTestRector (no faithful target). Register the rule in the
pest-to-testo set and refresh TODO.md / FEATURE_PARITY.md. 17 fixtures;
Bridge/Rector suite 107 -> 124, green.
Co-Authored-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
New
testo/bridge-rectorpackage — Rector rules to convert test suites between Testo, PHPUnit and Pest, in three direction sets:Expect::exception,throw SkipTest,#[Covers], lifecycle/group attributes, typed-chain decomposition, group-inheritance flattening.expectException,markTestSkipped,#[CoversClass], lifecycle methods,@dataProvider/#[DataProvider],@group/#[Group],DoesNotPerformAssertions.expect()->toX()→Assert::*.Conversions with no faithful counterpart (mocks, constraints, retry/repeat, Pest functional→class, …) are shipped as documented stub rules +
TODO.md, not silently dropped. AFEATURE_PARITY.mdmatrix tracks coverage.Supporting changes:
src/Testing/,RectorTestingPlugin): a rule carries#[TestRectorFixtures]pointing at co-located*.php.incfixtures, each run through a freshly-booted Rector container and reported as its own data set — no PHPUnit dependency.*.phpfile-type gate moved out ofTokenizer\Finderinto the locator middlewares (InlineFinder,BenchFinder,TestoAttributesLocatorInterceptor,NamingConventionLocator,RectorFixtureFinder), so each locator decides what it will load and co-located*.php.incfixtures are never reflected.bridge/rectorregistered for release-please + subtree-split publishing tophp-testo/bridge-rector.testo-migrate-from-phpunitreworked into a phased dual-path flow (Rector / AI-agent) with automation scripts;testo-plugin-authorsharpened.See commit history for details.
Why?
Testo is self-hosted: the engine that discovers and runs tests is the same code Infection mutates, so a mutation on the run path can break discovery itself and produce spurious survivors/kills instead of a real mutation signal. Converting the unit-style self-tests to PHPUnit lets Infection's PHPUnit adapter run them on a runner that shares no code with the mutated engine — a mutation can then only be caught (or missed) by an assertion, never by breaking the harness.
The bridge doubles as a migration path for downstream users moving onto Testo (and the reverse, and from Pest), which is what the reworked
testo-migrate-from-phpunitskill drives.Checklist