Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bridge/double/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
],
"require": {
"php": ">=8.3",
"jasonmccreary/double": "^0.7",
"jasonmccreary/double": "^0.8",
"testo/testo": "0.10.39 - 1"
},
"require-dev": {
Expand Down
50 changes: 50 additions & 0 deletions bridge/double/tests/Acceptance/DoubleBridgeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@

use JMac\Testing\Double;
use JMac\Testing\DoubleInterface;
use JMac\Testing\Matching\Argument;
use Testo\Assert;
use Testo\Bridge\Double\DoublePlugin;
use Testo\Codecov\CoversNothing;
use Testo\Test;
use Tests\Bridge\Double\Fixture\Adder;
use Tests\Bridge\Double\Fixture\Greeter;
use Tests\Bridge\Double\Fixture\Permissions;

/**
* Acceptance tests for {@see DoublePlugin}. The suite registers the plugin
Expand Down Expand Up @@ -50,4 +54,50 @@ public function spyRecordsCallsWithReceived(): void

$spy->received('count')->times(1);
}

public function jointArgumentMatchingWithArgumentAll(): void
{
// Argument::all() weighs the whole argument list at once: the call matches
// only because 2 < 7. The plugin verifies the expectation on teardown.
/** @var DoubleInterface&Adder $double */
$double = Double::for(Adder::class);
$double->expects('add')->with(Argument::all(fn(int $a, int $b): bool => $a < $b))->returns(9);

Assert::same($double->add(2, 7), 9);
}

public function overrideDoublesATargetWithAReservedNameCollision(): void
{
// Permissions::allows() collides with a Double control verb; override: true
// hands back an OverriddenDouble carrying the verbs, instance() the target-shaped double.
$permissions = Double::for(Permissions::class, override: true);
$permissions->expects('allows')->with('edit')->returns(true);

Assert::true($permissions->instance()->allows('edit'));
}

public function passthruSelfCallReachesAStub(): void
{
// greet()'s real body runs and its $this->normalize() self-call re-enters
// the double, so the stubbed normalize() answers instead of the real one.
/** @var DoubleInterface&Greeter $greeter */
$greeter = Double::for(Greeter::class);
$greeter->passthru();
$greeter->allows('normalize')->returns('WORLD');

Assert::same($greeter->greet('world'), 'Hello, WORLD');
}

public function aClonedDoubleSharesStateWithItsOriginal(): void
{
// The clone resolves to the same expectation state, so calling count() on it
// fulfills the expectation set on the original.
/** @var DoubleInterface&\Countable $double */
$double = Double::for(\Countable::class);
$double->expects('count')->returns(3);

$clone = clone $double;

Assert::same($clone->count(), 3);
}
}
15 changes: 15 additions & 0 deletions bridge/double/tests/Fixture/Adder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tests\Bridge\Double\Fixture;

/**
* A two-argument method, so a double can match its call with a joint
* predicate ({@see \JMac\Testing\Matching\Argument::all()}) that weighs both
* arguments against each other rather than one position at a time.
*/
interface Adder
{
public function add(int $a, int $b): int;
}
25 changes: 25 additions & 0 deletions bridge/double/tests/Fixture/Greeter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Tests\Bridge\Double\Fixture;

/**
* A concrete class whose {@see self::greet()} calls a sibling method on `$this`.
* Under a passthru double the real `greet()` body runs, so its self-call to
* `normalize()` re-enters the double and hits whatever stub is set for it.
*
* Not `final`: Double subclasses the target to build the passthru double.
*/
class Greeter
{
public function greet(string $name): string
{
return 'Hello, ' . $this->normalize($name);
}

public function normalize(string $name): string
{
return $name;
}
}
14 changes: 14 additions & 0 deletions bridge/double/tests/Fixture/Permissions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Tests\Bridge\Double\Fixture;

/**
* Declares a real `allows()` method, whose name collides with one of Double's
* control verbs. Doubling it needs `Double::for(..., override: true)`.
*/
interface Permissions
{
public function allows(string $ability): bool;
}
10 changes: 8 additions & 2 deletions bridge/rector/FEATURE_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Conversion coverage across the three directions supported by `testo/bridge-recto
| **Data providers** (`#[DataProvider]`/`#[DataSet]` ↔ `->with`) | ✅ *`DataProviderToPhpUnitRector` renames `#[\Testo\Data\DataProvider]` → `#[DataProvider]` and `#[\Testo\Data\DataSet([…], 'label')]` → `#[TestWith([…], 'label')]` (both repeatable, args verbatim)* | ✅ *both `@dataProvider` annotation **and** `#[DataProvider]` attribute → `#[\Testo\Data\DataProvider]`; cross-class external form left as TODO* | 🟡 *inline `->with([ rows ])` → one repeated `#[\Testo\Data\DataSet]` per row; a named `->with('x')` / `dataset()` definition needs a provider — TODO* |
| **Groups** (`#[Group]`) | ✅ *`GroupToPhpUnitRector` expands variadic → repeated `#[Group]`; `GroupInheritanceToPhpUnitRector` flattens both the class-level inheritance union (parents + traits) and the method-level prototype chain (a leaf method inherits the groups of the same-named parent-class method). Residual: traits are intentionally not consulted at method level — matches Testo, whose prototype walk skips them* | ✅ *`GroupToTestoRector` collapses `@group` annotations **and** repeated `#[Group]` into one variadic `#[\Testo\Filter\Group]`* | ✅ *`->group('a','b')` → `#[\Testo\Filter\Group('a','b')]`* |
| **ExpectNoAssertions** (`#[\Testo\Assert\ExpectNoAssertions]` ↔ `#[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions]`) | ✅ *`ExpectNoAssertionsToPhpUnitRector` (attribute rename; both sides method/function-level only — no fan-out)* | ✅ *`DoesNotPerformAssertionsToTestoRector` (attribute rename)* | ➖ |
| **Mocks** (`createMock`/`getMockBuilder`/`prophesize`) | ➖ | ⛔ *Testo has no built-in mocking* | ➖ |
| **Mocks** (`createMock`/`createStub` + `expects`/`method`/`will*`/`with`) | ➖ | 🟡 *`CreateMockToDoubleRector` converts onto the Double bridge (`testo/bridge-double`): `createMock`/`createStub` → `Double::for`, `createMockForIntersectionOfInterfaces([A, B])` → `Double::for(A, B)`, and the configuration chain onto `expects`/`allows`/`with`/`returns`/`throws`/`resolves` — the invocation matcher moves onto the verb (`once`→`times(1)`, `exactly`→`times`, `never`→`never`, `atLeastOnce`/`atLeast`/`atMost`→`times(minimum:/maximum:)`, `any`→`allows`), the method name off `->method()` onto `expects('m')`, the returns (incl. `willReturnArgument($n)`→`resolves(fn (...$a) => $a[$n])`, `willReturnSelf()`→`returns(<the double>)` and legacy `will(...)`), the builder chain `getMockBuilder(X)->disableOriginalConstructor()->getMock()`→`Double::for(X)`, and `with()` constraints onto `Argument::*` (`anything`→`any`, `identicalTo`→`same`, `isInstanceOf`/`isType`→`type`, `callback`→`satisfies`, `contains`→`contains`, `matchesRegularExpression`→`matches`; `equalTo($x)`→bare `$x`). All-or-nothing per chain: `willReturnMap`, a variable matcher, a builder step beyond `disableOriginalConstructor` (or the bare constructor-calling `getMockBuilder(X)->getMock()`), `prophesize`, and `with()` constraints with no `Argument` form (`stringContains`, `greaterThan`, `logicalOr`, …) leave the statement untouched — see `MockToTestoRector` (stub) and TODO.md* | ➖ |
| **Memory-leak expectations** | ⛔ *no PHPUnit equivalent* | ➖ | ➖ |
| **Retry / Repeat** (`#[Retry]`/`#[Repeat]`) | 🟡 *`RepeatRetryRector` converts `#[\Testo\Repeat]`/`#[\Testo\Retry]` → PHPUnit `#[Repeat]`/`#[Retry]` (PHPUnit 13.3+): `maxFailures`→`failureThreshold` (+1), Testo defaults made explicit. PHPUnit's are `TARGET_METHOD` only, so a class-level Testo attribute is fanned out onto each test method (a method's own attribute overrides it, not doubled); `markFlaky` is dropped (no PHPUnit equivalent)* | 🟡 *`RepeatRetryToTestoRector` converts `#[Repeat]`/`#[Retry]` → Testo's attributes: `failureThreshold`→`maxFailures` (−1; the default 1 folds to Testo's default 0 and is omitted)* | ➖ |
| **Fiber** (`#[RunInFiber]`, `Coroutine::spawn/await/concurrently`) | ⛔ *no PHPUnit/Pest equivalent — neither has a fiber/coroutine test attribute or an in-test coroutine scope* | ➖ | ➖ |
Expand Down Expand Up @@ -101,9 +101,15 @@ name from the description (kept as the docblock) and folding the fluent modifier
attributes / body statements. It bails (leaves the statement untouched) on a non-literal description,
a `use (...)`-capturing closure, or any unrecognised modifier — see `src/PestToTesto/TODO.md`.

The remaining ⛔ rows are intentionally out of scope: a missing target feature (mocking, `arch()`,
The remaining ⛔ rows are intentionally out of scope: a missing target feature (`arch()`,
memory-leak, PHPUnit `assertThat` constraints), the substring-vs-regex
exception-message mismatch, or Pest `uses()` (a function has no base class / traits / `$this`).
Mocks moved off this list: with the Double bridge there is now a target API, so `createMock`/
`createStub` (and intersection mocks), their `expects`/`method`/`will*` chains, and `with()`
constraints convert as a documented 🟡 (`CreateMockToDoubleRector`), including
`getMockBuilder(X)->disableOriginalConstructor()->getMock()` and `willReturnSelf()`; only the
unmappable links (`willReturnMap`, a builder step beyond `disableOriginalConstructor`, `prophesize`,
and `with()` constraints with no `Argument` form) stay manual.
Retry/Repeat moved off this list: PHPUnit 13.3 added `#[Repeat]`/`#[Retry]`, so both directions now
convert as a documented 🟡 (`RepeatRetryRector` / `RepeatRetryToTestoRector`).
PHPUnit's `markTestIncomplete` moved off this list — it now converts to a Skipped throw with an
Expand Down
5 changes: 5 additions & 0 deletions bridge/rector/config/phpunit-to-testo.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Rector\Config\RectorConfig;
use Testo\Bridge\Rector\PhpunitToTesto\AssertCallToTestoRector;
use Testo\Bridge\Rector\PhpunitToTesto\CoversClassToCoversRector;
use Testo\Bridge\Rector\PhpunitToTesto\CreateMockToDoubleRector;
use Testo\Bridge\Rector\PhpunitToTesto\DataProviderAnnotationToTestoRector;
use Testo\Bridge\Rector\PhpunitToTesto\DataProviderAttributeToTestoRector;
use Testo\Bridge\Rector\PhpunitToTesto\DoesNotPerformAssertionsToTestoRector;
Expand Down Expand Up @@ -61,4 +62,8 @@

# Repeat/Retry method attributes (PHPUnit 13.3+) map onto Testo's #[Repeat]/#[Retry].
$rectorConfig->rule(RepeatRetryToTestoRector::class);

# Mocks/stubs onto the Double bridge: createMock/createStub → Double::for, and the
# expects()/method()/will*() configuration chain onto expects/allows/with/returns/throws/resolves.
$rectorConfig->rule(CreateMockToDoubleRector::class);
};
Loading
Loading