[NetteTesterToPHPUnit] Add NetteTesterClassToPHPUnitClassRector

This commit is contained in:
Tomas Votruba 2019-03-16 17:01:40 +01:00
parent 8f8778124d
commit cae6f2a760
13 changed files with 619 additions and 42 deletions

View File

@ -1524,8 +1524,8 @@ Change assertContains()/assertNotContains() method to new string and iterable al
- $this->assertNotContains('foo', 'foo bar');
- $this->assertContains('foo', ['foo', 'bar']);
- $this->assertNotContains('foo', ['foo', 'bar']);
+ $this->assertStringContains('foo', 'foo bar');
+ $this->assertStringNotContains('foo', 'foo bar');
+ $this->assertStringContainsString('foo', 'foo bar');
+ $this->assertStringNotContainsString('foo', 'foo bar');
+ $this->assertIterableContains('foo', ['foo', 'bar']);
+ $this->assertIterableNotContains('foo', ['foo', 'bar']);
}

View File

@ -219,17 +219,18 @@ final class CreateRectorCommand extends Command implements ContributorCommandInt
*/
private function processComposerAutoload(array $templateVariables): void
{
$composerJsonContent = FileSystem::read(getcwd() . '/composer.json');
$composerJson = Json::decode($composerJsonContent, Json::FORCE_ARRAY);
$composerJsonFilePath = getcwd() . '/composer.json';
$composerJson = $this->loadFileToJson($composerJsonFilePath);
$package = $templateVariables['_Package_'];
// already autoloaded?
// skip core, already autoloaded
if ($package === 'Rector') {
return;
}
$package = $templateVariables['_Package_'];
$namespace = 'Rector\\' . $package . '\\';
$namespaceTest = 'Rector\\' . $package . '\\Tests\\';
@ -241,13 +242,7 @@ final class CreateRectorCommand extends Command implements ContributorCommandInt
$composerJson['autoload']['psr-4'][$namespace] = 'packages/' . $package . '/src';
$composerJson['autoload-dev']['psr-4'][$namespaceTest] = 'packages/' . $package . '/tests';
$composerJsonContent = Json::encode($composerJson, Json::PRETTY);
$composerJsonContent = $this->inlineSections($composerJsonContent, ['keywords', 'bin']);
// inline short arrays
FileSystem::write(getcwd() . '/composer.json', $composerJsonContent);
$this->saveJsonToFile($composerJsonFilePath, $composerJson);
}
/**
@ -267,4 +262,23 @@ final class CreateRectorCommand extends Command implements ContributorCommandInt
return $jsonContent;
}
/**
* @return mixed[]
*/
private function loadFileToJson(string $filePath): array
{
$fileContent = FileSystem::read($filePath);
return Json::decode($fileContent, Json::FORCE_ARRAY);
}
/**
* @param mixed[] $json
*/
private function saveJsonToFile(string $filePath, array $json): void
{
$content = Json::encode($json, Json::PRETTY);
$content = $this->inlineSections($content, ['keywords', 'bin']);
FileSystem::write($filePath, $content);
}
}

View File

@ -0,0 +1,340 @@
<?php declare(strict_types=1);
namespace Rector\NetteTesterToPHPUnit\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Expr\Cast\Bool_;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\Include_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
final class NetteTesterClassToPHPUnitClassRector extends AbstractRector
{
/**
* @var string
*/
private $netteTesterTestCaseClass;
/**
* @var ClassManipulator
*/
private $classManipulator;
/**
* @var CallableNodeTraverser
*/
private $callableNodeTraverser;
/**
* @var string[]
*/
private $assertMethodsRemap = [
'same' => 'assertSame',
'notSame' => 'assertNotSame',
'equal' => 'assertEqual',
'notEqual' => 'assertNotEqual',
'true' => 'assertTrue',
'false' => 'assertFalse',
'null' => 'assertNull',
'notNull' => 'assertNotNull',
'count' => 'assertCount',
'match' => 'assertStringMatchesFormat',
'matchFile' => 'assertStringMatchesFormatFile',
'contains' => 'assertContains',
'notContains' => 'assertNotContains',
'nan' => 'assertIsNumeric',
];
/**
* @var DocBlockManipulator
*/
private $docBlockManipulator;
public function __construct(
ClassManipulator $classManipulator,
CallableNodeTraverser $callableNodeTraverser,
DocBlockManipulator $docBlockManipulator,
string $netteTesterTestCaseClass = 'Tester\TestCase'
) {
$this->classManipulator = $classManipulator;
$this->callableNodeTraverser = $callableNodeTraverser;
$this->netteTesterTestCaseClass = $netteTesterTestCaseClass;
$this->docBlockManipulator = $docBlockManipulator;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Migrate Nette Tester test case to PHPUnit', [
new CodeSample(
<<<'CODE_SAMPLE'
namespace KdybyTests\Doctrine;
use Tester\TestCase;
use Tester\Assert;
require_once __DIR__ . '/../bootstrap.php';
class ExtensionTest extends TestCase
{
public function setUp()
{
}
public function testFunctionality()
{
Assert::true($default instanceof Kdyby\Doctrine\EntityManager);
Assert::true(5);
Assert::same($container->getService('kdyby.doctrine.default.entityManager'), $default);
}
}
(new \ExtensionTest())->run();
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
namespace KdybyTests\Doctrine;
use Tester\TestCase;
use Tester\Assert;
class ExtensionTest extends \PHPUnit\Framework\TestCase
{
protected function setUp()
{
}
public function testFunctionality()
{
self::assertInstanceOf(\Kdyby\Doctrine\EntityManager::cllass, $default);
self::assertTrue(5);
self::same($container->getService('kdyby.doctrine.default.entityManager'), $default);
}
}
CODE_SAMPLE
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Class_::class, Include_::class, MethodCall::class];
}
/**
* @param Class_|Include_|MethodCall $node
*/
public function refactor(Node $node): ?Node
{
if ($node instanceof Include_) {
$this->processAboveTestInclude($node);
return null;
}
if (! $this->isType($node, $this->netteTesterTestCaseClass)) {
return null;
}
if ($node instanceof MethodCall) {
$this->processUnderTestRun($node);
return null;
}
$this->processExtends($node);
$this->processMethods($node);
return $node;
}
private function processExtends(Class_ $class): void
{
$class->extends = new FullyQualified('PHPUnit\Framework\TestCase');
}
private function processAboveTestInclude(Include_ $include): void
{
if ($include->getAttribute(Attribute::CLASS_NODE) === null) {
$this->removeNode($include);
}
return;
}
private function processUnderTestRun(MethodCall $methodCall): void
{
if ($this->isName($methodCall, 'run')) {
$this->removeNode($methodCall);
}
return;
}
private function processMethods(Class_ $class): void
{
$methods = $this->classManipulator->getMethods($class);
foreach ($methods as $method) {
if ($this->isNamesInsensitive($method, ['setUp', 'tearDown'])) {
$this->makeProtected($method);
}
$this->processMethod($method);
}
}
private function renameMethods(StaticCall $staticCall): void
{
// special cases
if ($this->isNames($staticCall, ['exception', 'throws'])) {
$this->processExceptionStaticCall($staticCall);
return;
}
if ($this->isName($staticCall, 'type')) {
$this->processTypeStaticCall($staticCall);
return;
}
if ($this->isName($staticCall, 'noError')) {
$this->processNoErrorStaticCall($staticCall);
return;
}
if ($this->isNames($staticCall, ['truthy', 'falsey'])) {
$this->processTruthyOrFalseyStaticCall($staticCall);
return;
}
foreach ($this->assertMethodsRemap as $oldMethod => $newMethod) {
if ($this->isName($staticCall, $oldMethod)) {
$staticCall->name = new Identifier($newMethod);
continue;
}
}
}
private function processMethod(ClassMethod $classMethod): void
{
$this->callableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) {
if (! $node instanceof StaticCall) {
return null;
}
if (! $this->isType($node->class, 'Tester\Assert')) {
return null;
}
$node->class = new Name('self');
$this->renameMethods($node);
});
}
private function processExceptionStaticCall(StaticCall $staticCall): void
{
// expect exception
$expectException = new StaticCall(new Name('self'), 'expectException');
$expectException->args[] = $staticCall->args[1];
$this->addNodeAfterNode($expectException, $staticCall);
// expect message
if (isset($staticCall->args[2])) {
$expectExceptionMessage = new StaticCall(new Name('self'), 'expectExceptionMessage');
$expectExceptionMessage->args[] = $staticCall->args[2];
$this->addNodeAfterNode($expectExceptionMessage, $staticCall);
}
// expect code
if (isset($staticCall->args[3])) {
$expectExceptionMessage = new StaticCall(new Name('self'), 'expectExceptionCode');
$expectExceptionMessage->args[] = $staticCall->args[3];
$this->addNodeAfterNode($expectExceptionMessage, $staticCall);
}
/** @var Closure $callable */
$callable = $staticCall->args[0]->value;
foreach ((array) $callable->stmts as $callableStmt) {
$this->addNodeAfterNode($callableStmt, $staticCall);
}
$this->removeNode($staticCall);
}
private function processNoErrorStaticCall(StaticCall $staticCall): void
{
/** @var Closure $callable */
$callable = $staticCall->args[0]->value;
foreach ((array) $callable->stmts as $callableStmt) {
$this->addNodeAfterNode($callableStmt, $staticCall);
}
$this->removeNode($staticCall);
$methodNode = $staticCall->getAttribute(Attribute::METHOD_NODE);
if ($methodNode === null) {
return;
}
$phpDocTagNode = new PhpDocTextNode('@doesNotPerformAssertions');
$this->docBlockManipulator->addTag($methodNode, $phpDocTagNode);
}
private function processTruthyOrFalseyStaticCall(StaticCall $staticCall): void
{
if (! $this->isBoolType($staticCall->args[0]->value)) {
$staticCall->args[0]->value = new Bool_($staticCall->args[0]->value);
}
if ($this->isName($staticCall, 'truthy')) {
$staticCall->name = new Identifier('assertTrue');
} else {
$staticCall->name = new Identifier('assertFalse');
}
}
private function processTypeStaticCall(StaticCall $staticCall): void
{
$value = $this->getValue($staticCall->args[0]->value);
$typeToMethod = [
'list' => 'assertIsArray',
'array' => 'assertIsArray',
'bool' => 'assertIsBool',
'callable' => 'assertIsCallable',
'float' => 'assertIsFloat',
'int' => 'assertIsInt',
'integer' => 'assertIsInt',
'object' => 'assertIsObject',
'resource' => 'assertIsResource',
'string' => 'assertIsString',
'scalar' => 'assertIsScalar',
];
if (isset($typeToMethod[$value])) {
$staticCall->name = new Identifier($typeToMethod[$value]);
unset($staticCall->args[0]);
array_values($staticCall->args);
} elseif ($value === 'null') {
$staticCall->name = new Identifier('assertNull');
unset($staticCall->args[0]);
array_values($staticCall->args);
} else {
$staticCall->name = new Identifier('assertInstanceOf');
}
}
}

View File

@ -0,0 +1,3 @@
<?php declare(strict_types=1);
// just for the test

View File

@ -0,0 +1,65 @@
<?php
namespace Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Fixture;
namespace KdybyTests\Doctrine;
use Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Source\NetteTesterTestCase;
use Tester\Assert;
class AssertTypeTest extends NetteTesterTestCase
{
public function testFunctionality()
{
$value = 'SomeValue';
Assert::type('list', $value);
Assert::type('array', $value);
Assert::type('bool', $value);
Assert::type('callable', $value);
Assert::type('float', $value);
Assert::type('int', $value);
Assert::type('integer', $value);
Assert::type('null', $value);
Assert::type('object', $value);
Assert::type('resource', $value);
Assert::type('scalar', $value);
Assert::type('string', $value);
Assert::type(\stdClass::class, $value);
}
}
?>
-----
<?php
namespace Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Fixture;
namespace KdybyTests\Doctrine;
use Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Source\NetteTesterTestCase;
use Tester\Assert;
class AssertTypeTest extends \PHPUnit\Framework\TestCase
{
public function testFunctionality()
{
$value = 'SomeValue';
self::assertIsArray($value);
self::assertIsArray($value);
self::assertIsBool($value);
self::assertIsCallable($value);
self::assertIsFloat($value);
self::assertIsInt($value);
self::assertIsInt($value);
self::assertNull($value);
self::assertIsObject($value);
self::assertIsResource($value);
self::assertIsScalar($value);
self::assertIsString($value);
self::assertInstanceOf(\stdClass::class, $value);
}
}
?>

View File

@ -0,0 +1,99 @@
<?php
namespace Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Fixture;
namespace KdybyTests\Doctrine;
use Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Source\NetteTesterTestCase;
use Tester\Assert;
require_once __DIR__ . '/bootstrap.php';
class ExtensionTest extends NetteTesterTestCase
{
public function setUp()
{
}
public function testFunctionality()
{
$value = 'SomeValue';
Assert::type(\Kdyby\Doctrine\EntityManager::class, $value);
Assert::false(5);
Assert::same('ExpectedValue', $value);
}
public function testExceptions()
{
Assert::exception(function () {
$builder = new DI\ContainerBuilder;
$builder->run();
}, 'ExceptionClass', "Service 'one': Class or interface 'X' not found.", 200);
}
public function testNoError()
{
Assert::noError(function () {
$value = 1;
});
}
public function testY()
{
Assert::falsey('value', 'some messsage');
Assert::truthy(true);
}
}
(new ExtensionTest())->run();
?>
-----
<?php
namespace Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Fixture;
namespace KdybyTests\Doctrine;
use Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Source\NetteTesterTestCase;
use Tester\Assert;
class ExtensionTest extends \PHPUnit\Framework\TestCase
{
protected function setUp()
{
}
public function testFunctionality()
{
$value = 'SomeValue';
self::assertInstanceOf(\Kdyby\Doctrine\EntityManager::class, $value);
self::assertFalse(5);
self::assertSame('ExpectedValue', $value);
}
public function testExceptions()
{
self::expectException('ExceptionClass');
self::expectExceptionMessage("Service 'one': Class or interface 'X' not found.");
self::expectExceptionCode(200);
$builder = new DI\ContainerBuilder;
$builder->run();
}
/**
* @doesNotPerformAssertions
*/
public function testNoError()
{
$value = 1;
}
public function testY()
{
self::assertFalse((bool) 'value', 'some messsage');
self::assertTrue(true);
}
}
?>

View File

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector;
use Nette\Utils\FileSystem;
use Rector\NetteTesterToPHPUnit\Rector\Class_\NetteTesterClassToPHPUnitClassRector;
use Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Source\NetteTesterTestCase;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class NetteTesterClassToPHPUnitClassRectorTest extends AbstractRectorTestCase
{
public function test(): void
{
// prepare dummy data
FileSystem::copy(__DIR__ . '/Copy', $this->getTempPath());
$this->doTestFiles([__DIR__ . '/Fixture/fixture.php.inc', __DIR__ . '/Fixture/assert_type.php.inc']);
}
protected function getRectorClass(): string
{
return NetteTesterClassToPHPUnitClassRector::class;
}
/**
* @return string[]|null
*/
protected function getRectorConfiguration(): ?array
{
return [
'$netteTesterTestCaseClass' => NetteTesterTestCase::class,
];
}
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Rector\NetteTesterToPHPUnit\Tests\Rector\Class_\NetteTesterClassToPHPUnitClassRector\Source;
abstract class NetteTesterTestCase
{
public function run()
{
}
}

View File

@ -51,8 +51,8 @@ final class SomeTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$this->assertStringContains('foo', 'foo bar');
$this->assertStringNotContains('foo', 'foo bar');
$this->assertStringContainsString('foo', 'foo bar');
$this->assertStringNotContainsString('foo', 'foo bar');
}
}
CODE_SAMPLE

View File

@ -11,6 +11,7 @@ use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Namespace_;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\Util\RectorStrings;
use Symplify\PackageBuilder\Strings\StringFormatConverter;
@ -21,13 +22,24 @@ final class PhpSpecRenaming
*/
private $stringFormatConverter;
public function __construct(StringFormatConverter $stringFormatConverter)
/**
* @var NameResolver
*/
private $nameResolver;
public function __construct(StringFormatConverter $stringFormatConverter, NameResolver $nameResolver)
{
$this->stringFormatConverter = $stringFormatConverter;
$this->nameResolver = $nameResolver;
}
public function renameMethod(ClassMethod $classMethod, string $name): void
public function renameMethod(ClassMethod $classMethod): void
{
$name = $this->nameResolver->resolve($classMethod);
if ($name === null) {
return;
}
$name = RectorStrings::removePrefixes(
$name,
['it_should_have_', 'it_should_be', 'it_should_', 'it_is_', 'it_', 'is_']

View File

@ -230,23 +230,21 @@ CODE_SAMPLE
private function processMethods(Class_ $class): void
{
$methods = $this->classManipulator->getMethodsByName($class);
// let → setUp
foreach ($methods as $name => $method) {
if ($name === 'let') {
foreach ($this->classManipulator->getMethods($class) as $method) {
if ($this->isName($method, 'let')) {
$this->processLetMethod($method);
} else {
/** @var string $name */
$this->processTestMethod($method, $name);
$this->processTestMethod($method);
}
}
}
private function processTestMethod(ClassMethod $classMethod, string $name): void
private function processTestMethod(ClassMethod $classMethod): void
{
// change name to phpunit test case format
$this->phpSpecRenaming->renameMethod($classMethod, $name);
$this->phpSpecRenaming->renameMethod($classMethod);
// replace "$this" with "$this->{testedObject}"
$this->callableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) {

View File

@ -2,6 +2,7 @@
namespace Rector\PhpParser\Node\Manipulator;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
@ -232,15 +233,11 @@ final class ClassManipulator
/**
* @return ClassMethod[]
*/
public function getMethodsByName(Class_ $classNode): array
public function getMethods(Class_ $class): array
{
$methodsByName = [];
foreach ($classNode->stmts as $stmt) {
if ($stmt instanceof ClassMethod) {
$methodsByName[(string) $stmt->name] = $stmt;
}
}
return $methodsByName;
return array_filter($class->stmts, function (Node $node) {
return $node instanceof ClassMethod;
});
}
private function tryInsertBeforeFirstMethod(Class_ $classNode, Stmt $stmt): bool

View File

@ -66,12 +66,8 @@ abstract class AbstractRectorTestCase extends AbstractKernelTestCase
protected function provideConfig(): string
{
if ($this->getRectorClass() !== '') { // use local if not overloaded
$hash = Strings::substring(
md5($this->getRectorClass() . Json::encode($this->getRectorConfiguration())),
0,
10
);
$configFileTempPath = sprintf(sys_get_temp_dir() . '/rector_temp_tests/config_%s.yaml', $hash);
$fixtureHash = $this->createFixtureHash();
$configFileTempPath = sprintf(sys_get_temp_dir() . '/rector_temp_tests/config_%s.yaml', $fixtureHash);
// cache for 2nd run, similar to original config one
if (file_exists($configFileTempPath)) {
@ -121,6 +117,11 @@ abstract class AbstractRectorTestCase extends AbstractKernelTestCase
$this->autoloadTestFixture = true;
}
protected function getTempPath(): string
{
return sys_get_temp_dir() . '/rector_temp_tests';
}
/**
* @return string[]
*/
@ -170,11 +171,15 @@ abstract class AbstractRectorTestCase extends AbstractKernelTestCase
{
$hash = Strings::substring(md5($smartFileInfo->getRealPath()), 0, 5);
return sprintf(
sys_get_temp_dir() . '/rector_temp_tests/%s_%s_%s',
$prefix,
$hash,
$smartFileInfo->getBasename('.inc')
return sprintf($this->getTempPath() . '/%s_%s_%s', $prefix, $hash, $smartFileInfo->getBasename('.inc'));
}
private function createFixtureHash(): string
{
return Strings::substring(
md5($this->getRectorClass() . Json::encode($this->getRectorConfiguration())),
0,
10
);
}
}