diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..6a4243f6ae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.phpunit.cache +vendor +tmp diff --git a/.gitignore b/.gitignore index f6b653c766..72ef66c5ca 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ composer.lock /vendor .phpunit.cache +build/coverage.xml +coverage-priv.xml tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..32ae9e029b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM php:8.4-cli-bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl git unzip libicu-dev $PHPIZE_DEPS \ + && docker-php-ext-install intl \ + && curl -fsSL https://github.com/krakjoe/pcov/archive/refs/tags/v1.0.12.tar.gz -o /tmp/pcov.tar.gz \ + && tar xzf /tmp/pcov.tar.gz -C /tmp \ + && cd /tmp/pcov-1.0.12 \ + && phpize \ + && ./configure \ + && make -j"$(nproc)" \ + && make install \ + && docker-php-ext-enable pcov \ + && rm -rf /tmp/pcov.tar.gz /tmp/pcov-1.0.12 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /app diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..c7fb9e6703 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + php: + build: . + working_dir: /app + volumes: + - .:/app + environment: + COMPOSER_ALLOW_SUPERUSER: 1 diff --git a/docker/check.sh b/docker/check.sh new file mode 100755 index 0000000000..ad1093c551 --- /dev/null +++ b/docker/check.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." +source docker/common.sh + +install_dependencies + +docker compose run --rm php composer validate --ansi +docker compose run --rm php vendor/bin/phpunit +docker compose run --rm php vendor/bin/phpstan analyse --memory-limit=512M --ansi +docker compose run --rm php vendor/bin/ecs check --ansi +docker compose run --rm php vendor/bin/composer-dependency-analyser diff --git a/docker/common.sh b/docker/common.sh new file mode 100755 index 0000000000..2cc7bf90ee --- /dev/null +++ b/docker/common.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +install_dependencies() { + if [[ ! -f vendor/bin/phpunit ]]; then + docker compose run --rm php composer update --no-interaction + return + fi + + docker compose run --rm php composer install --no-interaction \ + || docker compose run --rm php composer update --no-interaction +} diff --git a/docker/coverage.sh b/docker/coverage.sh new file mode 100755 index 0000000000..6650e1f39c --- /dev/null +++ b/docker/coverage.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." +source docker/common.sh + +install_dependencies +docker compose run --rm php vendor/bin/phpunit --configuration phpunit.coverage.xml "$@" diff --git a/docker/test.sh b/docker/test.sh new file mode 100755 index 0000000000..15cf4f6fdf --- /dev/null +++ b/docker/test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." +source docker/common.sh + +install_dependencies +docker compose run --rm php vendor/bin/phpunit diff --git a/phpstan.neon b/phpstan.neon index 8d4af5e8ef..622f3ce512 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -29,16 +29,6 @@ parameters: identifier: method.alreadyNarrowedType path: tests - # compatibility with PHPUnit 9 and 10 - - - identifier: arguments.count - path: src/Testing/MockWire.php - - # type from service definition - - - identifier: argument.type - path: src/Testing/MockWire.php - # magic contract - identifier: public.method.unused diff --git a/phpunit.coverage.xml b/phpunit.coverage.xml new file mode 100644 index 0000000000..d5118e3bc6 --- /dev/null +++ b/phpunit.coverage.xml @@ -0,0 +1,27 @@ + + + + + tests + tests/Testing/UnitTestFilePathsFinder/Fixture + + + + + + src + + + + + + + + + diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index af962ab76e..6849f3b64d 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -18,6 +18,7 @@ public function create(): Container $container = new Container(); $container->autodiscover(__DIR__ . '/../Command'); + $container->autodiscover(__DIR__ . '/../Testing/Command'); $container->service(Parser::class, static function (): Parser { $phpParserFactory = new ParserFactory(); diff --git a/src/Finder/FilesFinder.php b/src/Finder/FilesFinder.php index 18b687299c..15b013bfd4 100644 --- a/src/Finder/FilesFinder.php +++ b/src/Finder/FilesFinder.php @@ -90,14 +90,16 @@ public static function findJsonFiles(array $sources): array } } - $jsonFileFinder = Finder::create() - ->files() - ->in($directories) - ->name('*.json') - ->sortByName(); - - foreach ($jsonFileFinder->getIterator() as $fileInfo) { - $jsonFileInfos[] = $fileInfo; + if ($directories !== []) { + $jsonFileFinder = Finder::create() + ->files() + ->in($directories) + ->name('*.json') + ->sortByName(); + + foreach ($jsonFileFinder->getIterator() as $fileInfo) { + $jsonFileInfos[] = $fileInfo; + } } return $jsonFileInfos; diff --git a/src/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor.php b/src/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor.php index a672151c18..57c18a7749 100644 --- a/src/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor.php @@ -35,7 +35,8 @@ public function enterNode(Node $node): ?Node } // must be named - if (! $node->namespacedName instanceof Name) { + $namespacedName = $node->namespacedName ?? null; + if (! $namespacedName instanceof Name) { return null; } diff --git a/src/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor.php b/src/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor.php index c2523593b5..5a1d3b9806 100644 --- a/src/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor.php @@ -170,7 +170,7 @@ private function getClassName(): string throw new ShouldNotHappenException('Class_ node is missing'); } - $namespaceName = $this->currentClass->namespacedName; + $namespaceName = $this->currentClass->namespacedName ?? null; if (! $namespaceName instanceof Name) { throw new ShouldNotHappenException(); } diff --git a/src/PhpParser/NodeVisitor/NeedForFinalizeNodeVisitor.php b/src/PhpParser/NodeVisitor/NeedForFinalizeNodeVisitor.php index f95fbf1623..895a7bf423 100644 --- a/src/PhpParser/NodeVisitor/NeedForFinalizeNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/NeedForFinalizeNodeVisitor.php @@ -45,7 +45,8 @@ public function enterNode(Node $node): ?Node } // we need a name to make it work - if (! $node->namespacedName instanceof Name) { + $namespacedName = $node->namespacedName ?? null; + if (! $namespacedName instanceof Name) { return null; } diff --git a/src/Testing/MockWire.php b/src/Testing/MockWire.php index 2fba87a322..8af347f676 100644 --- a/src/Testing/MockWire.php +++ b/src/Testing/MockWire.php @@ -11,6 +11,7 @@ use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; +use Webmozart\Assert\Assert; /** * @api used in public @@ -112,16 +113,11 @@ private static function matchPassedMockOrCreate( } } - // fallback to directly created mock - // support for PHPUnit 10 and 9 - $testCaseReflectionClass = new ReflectionClass(TestCase::class); - $testCaseConstructor = $testCaseReflectionClass->getConstructor(); - if ($testCaseConstructor instanceof ReflectionMethod && $testCaseConstructor->getNumberOfRequiredParameters() > 0) { - $phpunitMocker = new PHPUnitMocker('testName'); - } else { - $phpunitMocker = new PHPUnitMocker(); - } + $phpunitMocker = new PHPUnitMocker('mock'); + + $parameterClassName = $reflectionParameter->getType()->getName(); + Assert::classExists($parameterClassName); - return $phpunitMocker->create($reflectionParameter->getType()->getName()); + return $phpunitMocker->create($parameterClassName); } } diff --git a/stubs/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/stubs/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php index 6f63401868..37feb5d4e0 100644 --- a/stubs/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/stubs/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -2,6 +2,8 @@ namespace Symfony\Bundle\FrameworkBundle\Test; -class KernelTestCase +use PHPUnit\Framework\TestCase; + +class KernelTestCase extends TestCase { } diff --git a/tests/CachedPhpParser/CachedPhpParserTest.php b/tests/CachedPhpParser/CachedPhpParserTest.php new file mode 100644 index 0000000000..b014abd298 --- /dev/null +++ b/tests/CachedPhpParser/CachedPhpParserTest.php @@ -0,0 +1,50 @@ +cachedPhpParser = $this->make(CachedPhpParser::class); + } + + public function testParseFileCachesResult(): void + { + $filePath = __DIR__ . '/../PhpParser/Finder/ClassConstFinder/Fixture/SomeClassWithConstants.php'; + + $firstParse = $this->cachedPhpParser->parseFile($filePath); + $secondParse = $this->cachedPhpParser->parseFile($filePath); + + $this->assertSame($firstParse, $secondParse); + $this->assertNotEmpty($firstParse); + } + + public function testParseError(): void + { + $tempFile = sys_get_temp_dir() . '/swiss-knife-parse-error-' . uniqid() . '.php'; + FileSystem::write($tempFile, 'expectException(RuntimeException::class); + $this->expectExceptionMessage('Could not parse file "' . $tempFile . '"'); + + $this->cachedPhpParser->parseFile($tempFile); + } finally { + FileSystem::delete($tempFile); + } + } +} diff --git a/tests/Command/AliceYamlFixturesToPhpCommandTest.php b/tests/Command/AliceYamlFixturesToPhpCommandTest.php new file mode 100644 index 0000000000..8d2ec4ae2a --- /dev/null +++ b/tests/Command/AliceYamlFixturesToPhpCommandTest.php @@ -0,0 +1,41 @@ +tempDirectory = sys_get_temp_dir() . '/swiss-knife-alice-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + } + + protected function tearDown(): void + { + FileSystem::delete($this->tempDirectory); + } + + public function testRun(): void + { + $yamlPath = $this->tempDirectory . '/fixture.yml'; + FileSystem::write($yamlPath, "SomeEntity:\n name: test\n", null); + + $command = $this->make(AliceYamlFixturesToPhpCommand::class); + $exitCode = $command->run([$this->tempDirectory]); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertFileExists($this->tempDirectory . '/fixture.php'); + $this->assertFileDoesNotExist($yamlPath); + } +} diff --git a/tests/Command/AliceYamlFixturesToPhpYamlExtensionTest.php b/tests/Command/AliceYamlFixturesToPhpYamlExtensionTest.php new file mode 100644 index 0000000000..039b4bec1f --- /dev/null +++ b/tests/Command/AliceYamlFixturesToPhpYamlExtensionTest.php @@ -0,0 +1,40 @@ +tempDirectory = sys_get_temp_dir() . '/swiss-knife-alice-yaml-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + } + + protected function tearDown(): void + { + FileSystem::delete($this->tempDirectory); + } + + public function testYamlExtension(): void + { + $yamlPath = $this->tempDirectory . '/fixture.yaml'; + FileSystem::write($yamlPath, "SomeEntity:\n name: test\n", null); + + $command = $this->make(AliceYamlFixturesToPhpCommand::class); + $exitCode = $command->run([$this->tempDirectory]); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertFileExists($this->tempDirectory . '/fixture.php'); + } +} diff --git a/tests/Command/CheckCommentedCodeCommandTest.php b/tests/Command/CheckCommentedCodeCommandTest.php new file mode 100644 index 0000000000..c154a5fd75 --- /dev/null +++ b/tests/Command/CheckCommentedCodeCommandTest.php @@ -0,0 +1,30 @@ +make(CheckCommentedCodeCommand::class); + + $exitCode = $command->run([__DIR__ . '/Fixture/CheckCommentedCode/Clean'], [], 4); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } + + public function testErrorWhenCommentedCodeFound(): void + { + $command = $this->make(CheckCommentedCodeCommand::class); + + $exitCode = $command->run([__DIR__ . '/Fixture/CheckCommentedCode/Commented'], [], 2); + + $this->assertSame(ExitCode::ERROR, $exitCode); + } +} diff --git a/tests/Command/CheckConflictsCommandTest.php b/tests/Command/CheckConflictsCommandTest.php new file mode 100644 index 0000000000..7b0e872b12 --- /dev/null +++ b/tests/Command/CheckConflictsCommandTest.php @@ -0,0 +1,57 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + chdir(__DIR__); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + } + + public function testSuccessWhenNoConflicts(): void + { + $command = $this->make(CheckConflictsCommand::class); + + $exitCode = $command->run(['Fixture/CheckConflicts/no-conflicts']); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } + + public function testErrorWhenConflictsFound(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-check-conflicts-' . uniqid(); + FileSystem::createDir($tempDirectory); + FileSystem::write($tempDirectory . '/conflict.txt', "<<<<<<< HEAD\n=======\n", null); + + try { + chdir($tempDirectory); + + $command = $this->make(CheckConflictsCommand::class); + $exitCode = $command->run(['.']); + + $this->assertSame(ExitCode::ERROR, $exitCode); + } finally { + chdir($this->originalCwd); + FileSystem::delete($tempDirectory); + } + } +} diff --git a/tests/Command/CommandMetadataTest.php b/tests/Command/CommandMetadataTest.php new file mode 100644 index 0000000000..da5297f0ef --- /dev/null +++ b/tests/Command/CommandMetadataTest.php @@ -0,0 +1,60 @@ +}> + */ + public static function provideCommands(): iterable + { + yield 'check-conflicts' => [CheckConflictsCommand::class]; + yield 'check-commented-code' => [CheckCommentedCodeCommand::class]; + yield 'dump-editorconfig' => [DumpEditorconfigCommand::class]; + yield 'finalize-classes' => [FinalizeClassesCommand::class]; + yield 'find-multi-classes' => [FindMultiClassesCommand::class]; + yield 'generate-symfony-config-builders' => [GenerateSymfonyConfigBuildersCommand::class]; + yield 'generate-symfony-smoke-tests' => [GenerateSymfonySmokeTestsCommand::class]; + yield 'namespace-to-psr-4' => [NamespaceToPSR4Command::class]; + yield 'pretty-json' => [PrettyJsonCommand::class]; + yield 'privatize-constants' => [PrivatizeConstantsCommand::class]; + yield 'search-regex' => [SearchRegexCommand::class]; + yield 'split-config-per-package' => [SplitSymfonyConfigToPerPackageCommand::class]; + yield 'spot-lazy-traits' => [SpotLazyTraitsCommand::class]; + yield 'alice-yaml-fixtures-to-php' => [AliceYamlFixturesToPhpCommand::class]; + yield 'detect-unit-tests' => [DetectUnitTestsCommand::class]; + } + + /** + * @param class-string $commandClass + */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideCommands')] + public function testMetadata(string $commandClass): void + { + $command = $this->make($commandClass); + + $this->assertNotEmpty($command->getName()); + $this->assertNotEmpty($command->getDescription()); + } +} diff --git a/tests/Command/DetectUnitTestsCommandTest.php b/tests/Command/DetectUnitTestsCommandTest.php new file mode 100644 index 0000000000..af511a72d8 --- /dev/null +++ b/tests/Command/DetectUnitTestsCommandTest.php @@ -0,0 +1,58 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-detect-unit-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + @unlink($this->tempDirectory . '/phpunit-unit-files.xml'); + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testRunWithUnitTests(): void + { + FileSystem::copy( + __DIR__ . '/../Testing/UnitTestFilePathsFinder/Fixture/RandomTest.php', + $this->tempDirectory . '/RandomTest.php' + ); + + $command = $this->make(DetectUnitTestsCommand::class); + $exitCode = $command->run([$this->tempDirectory]); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertFileExists($this->tempDirectory . '/phpunit-unit-files.xml'); + } + + public function testRunWithNoUnitTests(): void + { + $command = $this->make(DetectUnitTestsCommand::class); + $exitCode = $command->run([$this->tempDirectory]); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } +} diff --git a/tests/Command/DumpEditorconfigCommandTest.php b/tests/Command/DumpEditorconfigCommandTest.php new file mode 100644 index 0000000000..4c2e63c476 --- /dev/null +++ b/tests/Command/DumpEditorconfigCommandTest.php @@ -0,0 +1,55 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-editorconfig-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testSuccess(): void + { + $command = $this->make(DumpEditorconfigCommand::class); + + $exitCode = $command->run(); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertFileExists($this->tempDirectory . '/.editorconfig'); + } + + public function testErrorWhenAlreadyExists(): void + { + FileSystem::write($this->tempDirectory . '/.editorconfig', 'root = true', null); + + $command = $this->make(DumpEditorconfigCommand::class); + $exitCode = $command->run(); + + $this->assertSame(ExitCode::ERROR, $exitCode); + } +} diff --git a/tests/Command/FinalizeClassesCommandExtendedTest.php b/tests/Command/FinalizeClassesCommandExtendedTest.php new file mode 100644 index 0000000000..8c55cf4f48 --- /dev/null +++ b/tests/Command/FinalizeClassesCommandExtendedTest.php @@ -0,0 +1,40 @@ +make(FinalizeClassesCommand::class); + $exitCode = $command->run([$tempDirectory], dryRun: false, noProgress: true); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertStringContainsString('final class', FileSystem::read($tempDirectory . '/ToFinalize.php')); + } finally { + FileSystem::delete($tempDirectory); + } + } +} diff --git a/tests/Command/FinalizeClassesCommandProgressTest.php b/tests/Command/FinalizeClassesCommandProgressTest.php new file mode 100644 index 0000000000..2c6da7f75d --- /dev/null +++ b/tests/Command/FinalizeClassesCommandProgressTest.php @@ -0,0 +1,53 @@ +make(FinalizeClassesCommand::class); + $exitCode = $command->run([$tempDirectory], dryRun: true, noProgress: false); + + $this->assertSame(ExitCode::ERROR, $exitCode); + } finally { + FileSystem::delete($tempDirectory); + } + } + + public function testWithSkipMockedAndProgressBar(): void + { + $command = $this->make(FinalizeClassesCommand::class); + + $exitCode = $command->run( + [__DIR__ . '/Fixture/FinalizeClasses'], + dryRun: true, + skipMocked: true, + noProgress: false + ); + + $this->assertContains($exitCode, [ExitCode::SUCCESS, ExitCode::ERROR]); + } +} diff --git a/tests/Command/FinalizeClassesCommandTest.php b/tests/Command/FinalizeClassesCommandTest.php new file mode 100644 index 0000000000..a05b89c5df --- /dev/null +++ b/tests/Command/FinalizeClassesCommandTest.php @@ -0,0 +1,39 @@ +make(FinalizeClassesCommand::class); + + $exitCode = $command->run( + [__DIR__ . '/Fixture/FinalizeClasses'], + dryRun: true, + noProgress: true + ); + + $this->assertContains($exitCode, [ExitCode::SUCCESS, ExitCode::ERROR]); + } + + public function testDryRunWithSkipMocked(): void + { + $command = $this->make(FinalizeClassesCommand::class); + + $exitCode = $command->run( + [__DIR__ . '/Fixture/FinalizeClasses'], + dryRun: true, + skipMocked: true, + noProgress: true + ); + + $this->assertContains($exitCode, [ExitCode::SUCCESS, ExitCode::ERROR]); + } +} diff --git a/tests/Command/FindMultiClassesCommandTest.php b/tests/Command/FindMultiClassesCommandTest.php new file mode 100644 index 0000000000..a6ee03557a --- /dev/null +++ b/tests/Command/FindMultiClassesCommandTest.php @@ -0,0 +1,30 @@ +make(FindMultiClassesCommand::class); + + $exitCode = $command->run([__DIR__ . '/Fixture/FindMultiClasses/single'], []); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } + + public function testErrorWhenMultipleClassesFound(): void + { + $command = $this->make(FindMultiClassesCommand::class); + + $exitCode = $command->run([__DIR__ . '/../Finder/MultiClassFixture'], []); + + $this->assertSame(ExitCode::ERROR, $exitCode); + } +} diff --git a/tests/Command/Fixture/CheckCommentedCode/Clean/CleanClass.php b/tests/Command/Fixture/CheckCommentedCode/Clean/CleanClass.php new file mode 100644 index 0000000000..3fa8f70078 --- /dev/null +++ b/tests/Command/Fixture/CheckCommentedCode/Clean/CleanClass.php @@ -0,0 +1,13 @@ +extension('framework', [ + 'secret' => 'test', + ]); + $containerConfigurator->extension('doctrine', [ + 'dbal' => [ + 'url' => 'sqlite:///:memory:', + ], + ]); +}; diff --git a/tests/Command/GenerateSymfonyConfigBuildersCommandTest.php b/tests/Command/GenerateSymfonyConfigBuildersCommandTest.php new file mode 100644 index 0000000000..e5c15b7c2e --- /dev/null +++ b/tests/Command/GenerateSymfonyConfigBuildersCommandTest.php @@ -0,0 +1,19 @@ +make(GenerateSymfonyConfigBuildersCommand::class); + + $this->assertSame(ExitCode::ERROR, $command->run()); + } +} diff --git a/tests/Command/GenerateSymfonySmokeTestsCommandExtendedTest.php b/tests/Command/GenerateSymfonySmokeTestsCommandExtendedTest.php new file mode 100644 index 0000000000..89d85db985 --- /dev/null +++ b/tests/Command/GenerateSymfonySmokeTestsCommandExtendedTest.php @@ -0,0 +1,44 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-smoke-error-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + FileSystem::createDir($this->tempDirectory . '/tests/Unit/Smoke'); + FileSystem::write($this->tempDirectory . '/composer.json', '{"require":{"some/package":"^1.0"}}', null); + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testNoMatchingPackages(): void + { + $command = $this->make(GenerateSymfonySmokeTestsCommand::class); + + $this->assertSame(ExitCode::ERROR, $command->run()); + } +} diff --git a/tests/Command/GenerateSymfonySmokeTestsCommandTest.php b/tests/Command/GenerateSymfonySmokeTestsCommandTest.php new file mode 100644 index 0000000000..0b1c158a6a --- /dev/null +++ b/tests/Command/GenerateSymfonySmokeTestsCommandTest.php @@ -0,0 +1,59 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-smoke-tests-' . uniqid(); + FileSystem::createDir($this->tempDirectory . '/tests/Unit/Smoke'); + + FileSystem::write($this->tempDirectory . '/composer.json', Json::encode([ + 'require' => [ + 'symfony/dependency-injection' => '^7.0', + ], + 'autoload-dev' => [ + 'psr-4' => [ + 'App\\Tests\\' => 'tests/', + ], + ], + ]), null); + + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testRun(): void + { + $command = $this->make(GenerateSymfonySmokeTestsCommand::class); + + $exitCode = $command->run(); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertFileExists($this->tempDirectory . '/tests/Unit/Smoke/ServiceContainerTest.php'); + $this->assertFileExists($this->tempDirectory . '/tests/Unit/Smoke/AbstractContainerTestCase.php'); + } +} diff --git a/tests/Command/GenerateSymfonySmokeTestsSkipExistingTest.php b/tests/Command/GenerateSymfonySmokeTestsSkipExistingTest.php new file mode 100644 index 0000000000..7c6a2f6b8f --- /dev/null +++ b/tests/Command/GenerateSymfonySmokeTestsSkipExistingTest.php @@ -0,0 +1,49 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-smoke-skip-' . uniqid(); + FileSystem::createDir($this->tempDirectory . '/tests/Unit/Smoke'); + FileSystem::write($this->tempDirectory . '/tests/Unit/Smoke/ServiceContainerTest.php', 'tempDirectory . '/composer.json', Json::encode([ + 'require' => ['symfony/dependency-injection' => '^7.0'], + 'autoload-dev' => ['psr-4' => ['App\\Tests\\' => 'tests/']], + ]), null); + + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testSkipExistingFiles(): void + { + $command = $this->make(GenerateSymfonySmokeTestsCommand::class); + + $this->assertSame(ExitCode::SUCCESS, $command->run()); + } +} diff --git a/tests/Command/NamespaceToPSR4CommandTest.php b/tests/Command/NamespaceToPSR4CommandTest.php index 5859114239..441081073d 100644 --- a/tests/Command/NamespaceToPSR4CommandTest.php +++ b/tests/Command/NamespaceToPSR4CommandTest.php @@ -11,6 +11,8 @@ final class NamespaceToPSR4CommandTest extends AbstractTestCase { + private string $originalCwd; + private CLIRequestMapper $cliRequestMapper; private NamespaceToPSR4Command $namespaceToPSR4Command; @@ -19,10 +21,18 @@ protected function setUp(): void { parent::setUp(); + $originalCwd = getcwd(); + $this->originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + $this->cliRequestMapper = $this->make(CLIRequestMapper::class); $this->namespaceToPSR4Command = $this->make(NamespaceToPSR4Command::class); } + protected function tearDown(): void + { + chdir($this->originalCwd); + } + /** * @see https://github.com/rectorphp/swiss-knife/issues/124 */ @@ -38,4 +48,65 @@ public function testNamespaceRootOptionIsKeptAsString(): void $this->assertSame(['app', 'App\\'], $arguments); } + + public function testRunFixesIncorrectNamespace(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-namespace-' . uniqid(); + \Nette\Utils\FileSystem::createDir($tempDirectory . '/Sub'); + \Nette\Utils\FileSystem::write( + $tempDirectory . '/Sub/SomeClass.php', + <<<'PHP' +namespaceToPSR4Command->run('.', 'App\\Tests'); + + $this->assertSame(\Entropy\Console\Enum\ExitCode::SUCCESS, $exitCode); + + $contents = file_get_contents($tempDirectory . '/Sub/SomeClass.php'); + $this->assertStringContainsString('namespace App\\Tests\\Sub;', (string) $contents); + + \Nette\Utils\FileSystem::delete($tempDirectory); + } + + public function testRunWithCorrectNamespace(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-namespace-ok-' . uniqid(); + \Nette\Utils\FileSystem::createDir($tempDirectory); + \Nette\Utils\FileSystem::write( + $tempDirectory . '/SomeClass.php', + <<<'PHP' +namespaceToPSR4Command->run('.', 'App\\Tests'); + + $this->assertSame(\Entropy\Console\Enum\ExitCode::SUCCESS, $exitCode); + + \Nette\Utils\FileSystem::delete($tempDirectory); + } } diff --git a/tests/Command/PrettyJsonCommandTest.php b/tests/Command/PrettyJsonCommandTest.php new file mode 100644 index 0000000000..75a78a5f78 --- /dev/null +++ b/tests/Command/PrettyJsonCommandTest.php @@ -0,0 +1,46 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + chdir(__DIR__); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + } + + public function testDryRun(): void + { + $command = $this->make(PrettyJsonCommand::class); + + $exitCode = $command->run(['Fixture/PrettyJson'], dryRun: true); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } + + public function testErrorWhenNoJsonFiles(): void + { + $command = $this->make(PrettyJsonCommand::class); + + $exitCode = $command->run(['Fixture/PrettyJson/empty-dir']); + + $this->assertSame(ExitCode::ERROR, $exitCode); + } +} diff --git a/tests/Command/PrettyJsonCommandWriteTest.php b/tests/Command/PrettyJsonCommandWriteTest.php new file mode 100644 index 0000000000..7a86e07ebe --- /dev/null +++ b/tests/Command/PrettyJsonCommandWriteTest.php @@ -0,0 +1,38 @@ +tempDirectory = sys_get_temp_dir() . '/swiss-knife-pretty-json-' . uniqid(); + \Nette\Utils\FileSystem::createDir($this->tempDirectory); + \Nette\Utils\FileSystem::write($this->tempDirectory . '/ugly.json', '{"a":1}', null); + } + + protected function tearDown(): void + { + \Nette\Utils\FileSystem::delete($this->tempDirectory); + } + + public function testWritePrettyJson(): void + { + $command = $this->make(PrettyJsonCommand::class); + + $exitCode = $command->run([$this->tempDirectory . '/ugly.json'], dryRun: false); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertStringContainsString("\n", \Nette\Utils\FileSystem::read($this->tempDirectory . '/ugly.json')); + } +} diff --git a/tests/Command/PrivatizeConstantsCommandExtendedTest.php b/tests/Command/PrivatizeConstantsCommandExtendedTest.php new file mode 100644 index 0000000000..a86eb79580 --- /dev/null +++ b/tests/Command/PrivatizeConstantsCommandExtendedTest.php @@ -0,0 +1,150 @@ +make(PrivatizeConstantsCommand::class); + $exitCode = $command->run([$tempDirectory], dryRun: false); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertStringContainsString('private const VALUE', FileSystem::read($tempDirectory . '/WithConstant.php')); + } finally { + FileSystem::delete($tempDirectory); + } + } + + public function testDryRunWithPrivatizableConstant(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-priv-dry-' . uniqid(); + FileSystem::createDir($tempDirectory); + FileSystem::write($tempDirectory . '/WithConstant.php', <<<'PHP' +make(PrivatizeConstantsCommand::class); + $exitCode = $command->run([$tempDirectory], dryRun: true); + + $this->assertSame(ExitCode::ERROR, $exitCode); + } finally { + FileSystem::delete($tempDirectory); + } + } + + public function testExternalConstantUsage(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-priv-ext-' . uniqid(); + FileSystem::createDir($tempDirectory); + FileSystem::write($tempDirectory . '/WithPublicConstant.php', <<<'PHP' +make(PrivatizeConstantsCommand::class); + $exitCode = $command->run([$tempDirectory], dryRun: true); + + $this->assertContains($exitCode, [ExitCode::SUCCESS, ExitCode::ERROR]); + } finally { + FileSystem::delete($tempDirectory); + } + } + + public function testNoConstantsInFile(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-priv-none-' . uniqid(); + FileSystem::createDir($tempDirectory); + FileSystem::write($tempDirectory . '/NoConstants.php', <<<'PHP' +make(PrivatizeConstantsCommand::class); + $exitCode = $command->run([$tempDirectory]); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } finally { + FileSystem::delete($tempDirectory); + } + } +} diff --git a/tests/Command/PrivatizeConstantsCommandTest.php b/tests/Command/PrivatizeConstantsCommandTest.php new file mode 100644 index 0000000000..369300e208 --- /dev/null +++ b/tests/Command/PrivatizeConstantsCommandTest.php @@ -0,0 +1,39 @@ +make(PrivatizeConstantsCommand::class); + + $exitCode = $command->run( + [__DIR__ . '/Fixture/PrivatizeConstants'], + dryRun: true + ); + + $this->assertContains($exitCode, [ExitCode::SUCCESS, ExitCode::ERROR]); + } + + public function testEmptySources(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-privatize-empty-' . uniqid(); + mkdir($tempDirectory); + + try { + $command = $this->make(PrivatizeConstantsCommand::class); + $exitCode = $command->run([$tempDirectory]); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } finally { + rmdir($tempDirectory); + } + } +} diff --git a/tests/Command/SearchRegexCommandTest.php b/tests/Command/SearchRegexCommandTest.php new file mode 100644 index 0000000000..48ac594df5 --- /dev/null +++ b/tests/Command/SearchRegexCommandTest.php @@ -0,0 +1,24 @@ +make(SearchRegexCommand::class); + + $exitCode = $command->run( + '#class SearchRegexTarget#', + __DIR__ . '/Fixture/SearchRegex' + ); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } +} diff --git a/tests/Command/SearchRegexDefaultDirectoryTest.php b/tests/Command/SearchRegexDefaultDirectoryTest.php new file mode 100644 index 0000000000..bc102c489c --- /dev/null +++ b/tests/Command/SearchRegexDefaultDirectoryTest.php @@ -0,0 +1,37 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + chdir(__DIR__ . '/Fixture/SearchRegex'); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + } + + public function testDefaultProjectDirectory(): void + { + $command = $this->make(SearchRegexCommand::class); + + $exitCode = $command->run('#class SearchRegexTarget#'); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } +} diff --git a/tests/Command/SplitSymfonyConfigNoExtensionsTest.php b/tests/Command/SplitSymfonyConfigNoExtensionsTest.php new file mode 100644 index 0000000000..583e873d94 --- /dev/null +++ b/tests/Command/SplitSymfonyConfigNoExtensionsTest.php @@ -0,0 +1,39 @@ +import('services.php'); +}; +PHP, null); + + try { + $command = $this->make(SplitSymfonyConfigToPerPackageCommand::class); + $exitCode = $command->run($tempDirectory . '/config.php', $tempDirectory . '/out'); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } finally { + FileSystem::delete($tempDirectory); + } + } +} diff --git a/tests/Command/SplitSymfonyConfigToPerPackageCommandTest.php b/tests/Command/SplitSymfonyConfigToPerPackageCommandTest.php new file mode 100644 index 0000000000..242f658842 --- /dev/null +++ b/tests/Command/SplitSymfonyConfigToPerPackageCommandTest.php @@ -0,0 +1,46 @@ +tempDirectory = sys_get_temp_dir() . '/swiss-knife-split-config-' . uniqid(); + FileSystem::createDir($this->tempDirectory . '/packages'); + FileSystem::copy( + __DIR__ . '/Fixture/SplitSymfonyConfig/config.php', + $this->tempDirectory . '/config.php' + ); + } + + protected function tearDown(): void + { + FileSystem::delete($this->tempDirectory); + } + + public function testRun(): void + { + $command = $this->make(SplitSymfonyConfigToPerPackageCommand::class); + + $exitCode = $command->run( + $this->tempDirectory . '/config.php', + $this->tempDirectory . '/packages' + ); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + $this->assertFileExists($this->tempDirectory . '/packages/framework.php'); + $this->assertFileExists($this->tempDirectory . '/packages/doctrine.php'); + } +} diff --git a/tests/Command/SpotLazyTraitsCommandTest.php b/tests/Command/SpotLazyTraitsCommandTest.php new file mode 100644 index 0000000000..3125aad408 --- /dev/null +++ b/tests/Command/SpotLazyTraitsCommandTest.php @@ -0,0 +1,37 @@ +make(SpotLazyTraitsCommand::class); + + $exitCode = $command->run([__DIR__ . '/../Traits/Fixture'], maxUsed: 2); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } + + public function testWithoutTraits(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-no-traits-' . uniqid(); + FileSystem::createDir($tempDirectory); + + try { + $command = $this->make(SpotLazyTraitsCommand::class); + $exitCode = $command->run([$tempDirectory]); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } finally { + FileSystem::delete($tempDirectory); + } + } +} diff --git a/tests/Coverage/RemainingCoverageTest.php b/tests/Coverage/RemainingCoverageTest.php new file mode 100644 index 0000000000..96c46c3123 --- /dev/null +++ b/tests/Coverage/RemainingCoverageTest.php @@ -0,0 +1,572 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + } + + public function testNamespaceToPSR4SkipsCorrectNamespaceAndReportsAllCorrect(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-ns-all-ok-' . uniqid(); + FileSystem::createDir($tempDirectory); + FileSystem::write( + $tempDirectory . '/SomeClass.php', + "make(NamespaceToPSR4Command::class); + $exitCode = $command->run('.', 'App\\Tests'); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + FileSystem::delete($tempDirectory); + } + + public function testNamespaceToPSR4FixesMultipleFiles(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-ns-multi-' . uniqid(); + FileSystem::createDir($tempDirectory . '/A'); + FileSystem::createDir($tempDirectory . '/B'); + FileSystem::write($tempDirectory . '/A/One.php', "make(NamespaceToPSR4Command::class); + $exitCode = $command->run('.', 'App\\Tests'); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + FileSystem::delete($tempDirectory); + } + + public function testSearchRegexSkipsFilesWithoutMatches(): void + { + $command = $this->make(SearchRegexCommand::class); + $exitCode = $command->run('#class\\s+#', __DIR__ . '/../Finder/Fixture'); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } + + public function testPrivatizeConstantsKeepsPublicWhenUsedExternally(): void + { + $command = $this->make(PrivatizeConstantsCommand::class); + $exitCode = $command->run( + [__DIR__ . '/../PhpParser/Finder/ClassConstantFetchFinder/Fixture/Standard'], + dryRun: true + ); + + $this->assertSame(ExitCode::SUCCESS, $exitCode); + } + + public function testSplitSymfonyConfigThrowsOnNonStringExtensionArgument(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-split-bad-' . uniqid(); + FileSystem::createDir($tempDirectory); + $configPath = $tempDirectory . '/config.php'; + FileSystem::write( + $configPath, + <<<'PHP' +extension(123); +}; +PHP, + null + ); + + $command = $this->make(SplitSymfonyConfigToPerPackageCommand::class); + + $this->expectException(ShouldNotHappenException::class); + $command->run($configPath, $tempDirectory . '/out'); + + FileSystem::delete($tempDirectory); + } + + public function testPhpFilesFinderExcludesByExactFilePath(): void + { + $fixtureDirectory = __DIR__ . '/../Finder/Fixture'; + $controllerPath = $fixtureDirectory . '/AController.php'; + + $filesByExactPath = PhpFilesFinder::find([$fixtureDirectory], [realpath($controllerPath)]); + $this->assertCount(2, $filesByExactPath); + } + + public function testClassConstantFetchFinderDebugPrintsTraversalError(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-ccf-debug-' . uniqid(); + FileSystem::createDir($tempDirectory); + FileSystem::write( + $tempDirectory . '/Bad.php', + "make(ClassConstantFetchFinder::class); + $fileInfos = PhpFilesFinder::find([$tempDirectory]); + $progressBar = new ProgressBar(new OutputColorizer()); + $progressBar->start(1); + + $fetches = $finder->find($fileInfos, $progressBar, isDebug: true); + + $this->assertSame([], $fetches); + FileSystem::delete($tempDirectory); + } + + public function testAddImportConfigMethodCallVisitorSkipsClosureWithMultipleParameters(): void + { + $closure = new Closure(); + $closure->params = [new \PhpParser\Node\Param(new Variable('a')), new \PhpParser\Node\Param(new Variable('b'))]; + + $visitor = new AddImportConfigMethodCallNodeVisitor('/out'); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor($visitor); + $nodeTraverser->traverse([$closure]); + + $this->assertCount(0, $closure->stmts ?? []); + } + + public function testEntityClassNameCollectingNodeVisitorSkipsUnnamedClassAndDocWithoutAt(): void + { + $visitor = new EntityClassNameCollectingNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $visitorWithoutNameResolver = new EntityClassNameCollectingNodeVisitor(); + $nodeTraverserWithoutResolver = new NodeTraverser(); + $nodeTraverserWithoutResolver->addVisitor($visitorWithoutNameResolver); + $nodeTraverserWithoutResolver->traverse($this->parseCode('traverse($this->parseCode('assertSame([], $visitor->getEntityClassNames()); + } + + public function testExtractSymfonyExtensionCallNodeVisitorSkipsNonMethodCallAndDynamicName(): void + { + $notMethodCall = new Expression(new Variable('foo')); + $dynamicNameCall = new Expression(new MethodCall(new Variable('c'), new String_('extension'))); + + $visitor = new ExtractSymfonyExtensionCallNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor($visitor); + $nodeTraverser->traverse([$notMethodCall, $dynamicNameCall]); + + $this->assertSame([], $visitor->getExtensionMethodCalls()); + } + + public function testFindClassConstFetchNodeVisitorEdgeCases(): void + { + require_once __DIR__ . '/../PhpParser/Finder/ClassConstantFetchFinder/Fixture/Standard/AnotherClassWithConstant.php'; + + $dynamicClassFetch = <<<'PHP' +addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + $nodeTraverser->traverse($this->parseCode($dynamicClassFetch)); + + $staticCurrent = <<<'PHP' +addVisitor(new NameResolver()); + $nodeTraverser3->addVisitor($visitor3); + $nodeTraverser3->traverse($this->parseCode($staticCurrent)); + $this->assertCount(1, $visitor3->getClassConstantFetches()); + + $vendorParent = <<<'PHP' +addVisitor(new NameResolver()); + $nodeTraverserVendor->addVisitor($visitorVendor); + $nodeTraverserVendor->traverse($this->parseCode($vendorParent)); + $this->assertSame([], $visitorVendor->getClassConstantFetches()); + + $vendorExternal = <<<'PHP' +addVisitor(new NameResolver()); + $nodeTraverserExternal->addVisitor($visitorExternal); + $nodeTraverserExternal->traverse($this->parseCode($vendorExternal)); + $this->assertSame([], $visitorExternal->getClassConstantFetches()); + + if (! trait_exists('TraitFetch\\SomeTrait')) { + eval('namespace TraitFetch; trait SomeTrait { public const FOO = "bar"; }'); + } + + $traitFetch = <<<'PHP' +addVisitor(new NameResolver()); + $nodeTraverser4->addVisitor($visitor4); + $nodeTraverser4->traverse($this->parseCode($traitFetch)); + $this->assertCount(1, $visitor4->getClassConstantFetches()); + } + + public function testFindClassConstFetchNodeVisitorMissingParentThrows(): void + { + $missingParent = <<<'PHP' +addVisitor(new NameResolver()); + $nodeTraverser5->addVisitor($visitor5); + $this->expectException(ShouldNotHappenException::class); + $nodeTraverser5->traverse($this->parseCode($missingParent)); + } + + public function testFindClassConstFetchNodeVisitorGetClassNameThrowsWithoutNamespace(): void + { + $globalSelf = <<<'PHP' +addVisitor($visitor); + $this->expectException(ShouldNotHappenException::class); + $nodeTraverser->traverse($this->parseCode($globalSelf)); + } + + public function testFindNonPrivateClassConstNodeVisitorSkipsPrivateConstants(): void + { + $visitor = new FindNonPrivateClassConstNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $code = <<<'PHP' +traverse($this->parseCode($code)); + + $constants = $visitor->getClassConstants(); + $this->assertCount(1, $constants); + $this->assertSame('VISIBLE', $constants[0]->getConstantName()); + } + + public function testMockedClassNameCollectingNodeVisitorSkipsDynamicMethodAndMissingArgs(): void + { + $visitor = new MockedClassNameCollectingNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor($visitor); + + $code = <<<'PHP' +{'createMock'}(); + $this->createMock(); + } +} +PHP; + $nodeTraverser->traverse($this->parseCode($code)); + + $this->assertSame([], $visitor->getMockedClassNames()); + } + + public function testNeedForFinalizeNodeVisitorSkipsClassWithoutNamespace(): void + { + $visitor = new NeedForFinalizeNodeVisitor([]); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor($visitor); + + $nodeTraverser->traverse($this->parseCode('assertFalse($visitor->isNeeded()); + } + + public function testParentClassNameCollectingNodeVisitorFiltersVendorParents(): void + { + $visitor = new ParentClassNameCollectingNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $code = <<<'PHP' +traverse($this->parseCode($code)); + + $this->assertSame(['App\\Legit\\Parent'], $visitor->getParentClassNames()); + } + + public function testTemplateDecoratorUsesAppKernel(): void + { + if (! class_exists('App\\Kernel', false)) { + eval('namespace App; class Kernel {}'); + } + + $templateDecorator = new TemplateDecorator(); + $decorated = $templateDecorator->decorate('__KERNEL_CLASS_PLACEHOLDER__'); + + $this->assertStringContainsString('App\\Kernel', $decorated); + } + + #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] + public function testTemplateDecoratorUsesLegacyAppKernel(): void + { + if (! class_exists('AppKernel', false)) { + eval('class AppKernel {}'); + } + + $templateDecorator = new TemplateDecorator(); + $decorated = $templateDecorator->decorate('__KERNEL_CLASS_PLACEHOLDER__'); + + $this->assertStringContainsString('AppKernel', $decorated); + } + + public function testFindClassConstFetchNodeVisitorGetClassNameThrowsWhenCurrentClassMissing(): void + { + $visitor = new FindClassConstFetchNodeVisitor(); + $reflectionMethod = new \ReflectionMethod(FindClassConstFetchNodeVisitor::class, 'getClassName'); + $reflectionMethod->setAccessible(true); + + $this->expectException(ShouldNotHappenException::class); + $reflectionMethod->invoke($visitor); + } + + public function testFindClassConstFetchNodeVisitorDoesClassExistForInterface(): void + { + $visitor = new FindClassConstFetchNodeVisitor(); + $reflectionMethod = new \ReflectionMethod(FindClassConstFetchNodeVisitor::class, 'doesClassExist'); + $reflectionMethod->setAccessible(true); + + $this->assertTrue($reflectionMethod->invoke($visitor, 'Countable')); + } + + public function testTestCaseClassFinderSkipsExistingInterfaceAndTrait(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-tccf-' . uniqid(); + FileSystem::createDir($tempDirectory); + FileSystem::write($tempDirectory . '/SomeInterface.php', 'findInDirectories([$tempDirectory]); + $classesAgain = $finder->findInDirectories([$tempDirectory]); + + $this->assertArrayHasKey('SomeTestInterface', $classes); + $this->assertArrayHasKey('SomeTestTrait', $classes); + $this->assertArrayHasKey('SomeTestInterface', $classesAgain); + $this->assertArrayHasKey('SomeTestTrait', $classesAgain); + + FileSystem::delete($tempDirectory); + } + + public function testUnitTestFilterExcludesKernelTestCase(): void + { + if (! class_exists(\Symfony\Bundle\FrameworkBundle\Test\KernelTestCase::class)) { + eval('namespace Symfony\Bundle\FrameworkBundle\Test; class KernelTestCase extends \PHPUnit\Framework\TestCase {}'); + } + + eval('namespace SwissKnifeKernelTest; class MyKernelTest extends \Symfony\Bundle\FrameworkBundle\Test\KernelTestCase {}'); + + $unitTestFilter = new UnitTestFilter(); + $isUnitTest = new \ReflectionMethod(UnitTestFilter::class, 'isUnitTest'); + $isUnitTest->setAccessible(true); + + $this->assertFalse($isUnitTest->invoke($unitTestFilter, 'SwissKnifeKernelTest\\MyKernelTest')); + + $filtered = $unitTestFilter->filter([ + 'SwissKnifeKernelTest\\MyKernelTest' => '/path/MyKernelTest.php', + 'PHPUnit\\Framework\\TestCase' => '/path/TestCase.php', + ]); + + $this->assertSame(['PHPUnit\\Framework\\TestCase' => '/path/TestCase.php'], $filtered); + } + + public function testTraitSpotterSkipsUnknownTraitUsage(): void + { + $tempDirectory = sys_get_temp_dir() . '/swiss-knife-trait-' . uniqid(); + FileSystem::createDir($tempDirectory); + FileSystem::write($tempDirectory . '/KnownTrait.php', "make(TraitSpotter::class); + $result = $traitSpotter->analyse([$tempDirectory]); + + $this->assertSame(1, $result->getTraitCount()); + + FileSystem::delete($tempDirectory); + } + + public function testIsClassConstantUsedPubliclyBranches(): void + { + $command = $this->make(PrivatizeConstantsCommand::class); + + $externalFetch = new ExternalClassAccessConstantFetch('SomeClass', 'FOO'); + $currentFetch = new CurrentClassConstantFetch('SomeClass', 'FOO'); + + $reflectionMethod = new \ReflectionMethod(PrivatizeConstantsCommand::class, 'isClassConstantUsedPublicly'); + $reflectionMethod->setAccessible(true); + + $classConstant = new \Rector\SwissKnife\ValueObject\ClassConstant('OtherClass', 'BAR'); + + $isPublic = $reflectionMethod->invoke($command, [$externalFetch, $currentFetch], $classConstant); + $this->assertFalse($isPublic); + + $matchingExternal = new \Rector\SwissKnife\ValueObject\ClassConstant('SomeClass', 'FOO'); + $isPublicExternal = $reflectionMethod->invoke($command, [$externalFetch], $matchingExternal); + $this->assertTrue($isPublicExternal); + } + + /** + * @return array<\PhpParser\Node> + */ + private function parseCode(string $code): array + { + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $nodes = $parser->parse($code); + $this->assertNotNull($nodes); + + return $nodes; + } +} diff --git a/tests/DependencyInjection/ContainerFactoryTest.php b/tests/DependencyInjection/ContainerFactoryTest.php new file mode 100644 index 0000000000..d8675ac91d --- /dev/null +++ b/tests/DependencyInjection/ContainerFactoryTest.php @@ -0,0 +1,36 @@ +create(); + + $this->assertInstanceOf(Container::class, $container); + $this->assertInstanceOf(Parser::class, $container->make(Parser::class)); + $this->assertInstanceOf(CheckConflictsCommand::class, $container->make(CheckConflictsCommand::class)); + $this->assertInstanceOf(DetectUnitTestsCommand::class, $container->make(DetectUnitTestsCommand::class)); + } + + public function testCommandsImplementCommandInterface(): void + { + $containerFactory = new ContainerFactory(); + $container = $containerFactory->create(); + + $checkConflictsCommand = $container->make(CheckConflictsCommand::class); + $this->assertInstanceOf(CommandInterface::class, $checkConflictsCommand); + } +} diff --git a/tests/Enum/StaticAccessorTest.php b/tests/Enum/StaticAccessorTest.php new file mode 100644 index 0000000000..11e11ae90a --- /dev/null +++ b/tests/Enum/StaticAccessorTest.php @@ -0,0 +1,17 @@ +assertSame('static', StaticAccessor::STATIC); + $this->assertSame('self', StaticAccessor::SELF); + } +} diff --git a/tests/Enum/SymfonyClassTest.php b/tests/Enum/SymfonyClassTest.php new file mode 100644 index 0000000000..a92ab80250 --- /dev/null +++ b/tests/Enum/SymfonyClassTest.php @@ -0,0 +1,17 @@ +assertSame(ContainerConfigurator::class, SymfonyClass::CONTAINER_CONFIGURATOR_CLASS); + } +} diff --git a/tests/FileSystem/JsonAnalyzerTest.php b/tests/FileSystem/JsonAnalyzerTest.php new file mode 100644 index 0000000000..0741e996f1 --- /dev/null +++ b/tests/FileSystem/JsonAnalyzerTest.php @@ -0,0 +1,26 @@ +jsonAnalyzer = new JsonAnalyzer(); + } + + public function testIsPrettyPrinted(): void + { + $this->assertFalse($this->jsonAnalyzer->isPrettyPrinted('{"a":1}')); + + $prettyJson = "{\n \"a\": 1\n}"; + $this->assertTrue($this->jsonAnalyzer->isPrettyPrinted($prettyJson)); + } +} diff --git a/tests/FileSystem/PathHelperTest.php b/tests/FileSystem/PathHelperTest.php new file mode 100644 index 0000000000..b49a837b5c --- /dev/null +++ b/tests/FileSystem/PathHelperTest.php @@ -0,0 +1,41 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-path-helper-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testRelativeToCwd(): void + { + $filePath = $this->tempDirectory . '/some/nested/file.php'; + FileSystem::createDir(dirname($filePath)); + FileSystem::write($filePath, 'assertSame('some/nested/file.php', PathHelper::relativeToCwd($filePath)); + } +} diff --git a/tests/Finder/FilesFinderExtendedTest.php b/tests/Finder/FilesFinderExtendedTest.php new file mode 100644 index 0000000000..27ccfdaefa --- /dev/null +++ b/tests/Finder/FilesFinderExtendedTest.php @@ -0,0 +1,52 @@ +tempDirectory = sys_get_temp_dir() . '/swiss-knife-files-finder-' . uniqid(); + FileSystem::createDir($this->tempDirectory . '/twig'); + FileSystem::write($this->tempDirectory . '/twig/template.twig', 'content', null); + FileSystem::write($this->tempDirectory . '/single.json', '{"a":1}', null); + FileSystem::write($this->tempDirectory . '/fixture.yml', 'key: value', null); + } + + protected function tearDown(): void + { + FileSystem::delete($this->tempDirectory); + } + + public function testFindTwigFiles(): void + { + $files = FilesFinder::findTwigFiles([$this->tempDirectory]); + + $this->assertCount(1, $files); + } + + public function testFindJsonFilesWithSingleFile(): void + { + $jsonFile = realpath($this->tempDirectory . '/single.json'); + $this->assertNotFalse($jsonFile); + + $files = FilesFinder::findJsonFiles([$jsonFile]); + + $this->assertCount(1, $files); + } + + public function testFindYamlFiles(): void + { + $files = FilesFinder::findYamlFiles([$this->tempDirectory]); + + $this->assertCount(1, $files); + } +} diff --git a/tests/Finder/MultiClassFixture/TwoClassesInOneFile.php b/tests/Finder/MultiClassFixture/TwoClassesInOneFile.php new file mode 100644 index 0000000000..a877a9b09b --- /dev/null +++ b/tests/Finder/MultiClassFixture/TwoClassesInOneFile.php @@ -0,0 +1,13 @@ +make(MultipleClassInOneFileFinder::class); + + $multipleClassesByFile = $multipleClassInOneFileFinder->findInDirectories( + [__DIR__ . '/MultiClassFixture'], + [] + ); + + $twoClassesFilePath = __DIR__ . '/MultiClassFixture/TwoClassesInOneFile.php'; + $this->assertArrayHasKey($twoClassesFilePath, $multipleClassesByFile); + $this->assertCount(2, $multipleClassesByFile[$twoClassesFilePath]); + } +} diff --git a/tests/Git/ConflictResolver/ConflictResolverExtractFromFileInfosTest.php b/tests/Git/ConflictResolver/ConflictResolverExtractFromFileInfosTest.php new file mode 100644 index 0000000000..33e8e3c596 --- /dev/null +++ b/tests/Git/ConflictResolver/ConflictResolverExtractFromFileInfosTest.php @@ -0,0 +1,41 @@ +conflictResolver = new ConflictResolver(); + } + + public function testExtractFromFileInfosSkipsFixtures(): void + { + $fixturePath = __DIR__ . '/Fixture/some_file.txt'; + $outsidePath = sys_get_temp_dir() . '/swiss-knife-conflict-' . uniqid() . '.txt'; + file_put_contents($outsidePath, "<<<<<<< HEAD\ncontent\n=======\n"); + + try { + $result = $this->conflictResolver->extractFromFileInfos([$fixturePath, $outsidePath]); + + $this->assertArrayNotHasKey($fixturePath, $result); + $this->assertSame([$outsidePath => 1], $result); + } finally { + @unlink($outsidePath); + } + } + + public function testExtractFromFileInfosSkipsFilesWithoutConflicts(): void + { + $correctFilePath = __DIR__ . '/Fixture/correct_file.txt'; + + $this->assertSame([], $this->conflictResolver->extractFromFileInfos([$correctFilePath])); + } +} diff --git a/tests/MockedClassResolver/MockedClassResolverTest.php b/tests/MockedClassResolver/MockedClassResolverTest.php new file mode 100644 index 0000000000..b7841839bc --- /dev/null +++ b/tests/MockedClassResolver/MockedClassResolverTest.php @@ -0,0 +1,37 @@ +make(MockedClassResolver::class); + + $fixtureDirectory = __DIR__ . '/../PhpParser/NodeVisitor/MockedClassNameCollectingNodeVisitor/Fixture'; + $progressCalls = 0; + + $mockedClassNames = $mockedClassResolver->resolve( + [$fixtureDirectory], + static function () use (&$progressCalls): void { + ++$progressCalls; + } + ); + + $this->assertSame( + [ + 'SomeNamespace\SomeConfiguredStubClass', + 'SomeNamespace\SomeIntersectionClass', + 'SomeNamespace\SomeMockedClass', + 'SomeNamespace\SomeStubClass', + ], + $mockedClassNames + ); + $this->assertGreaterThan(0, $progressCalls); + } +} diff --git a/tests/PhpParser/Finder/ClassConstantFetchFinder/ClassConstantFetchFinderTest.php b/tests/PhpParser/Finder/ClassConstantFetchFinder/ClassConstantFetchFinderTest.php index f2e8a2de71..9670f07d88 100644 --- a/tests/PhpParser/Finder/ClassConstantFetchFinder/ClassConstantFetchFinderTest.php +++ b/tests/PhpParser/Finder/ClassConstantFetchFinder/ClassConstantFetchFinderTest.php @@ -13,6 +13,8 @@ use Rector\SwissKnife\Tests\AbstractTestCase; use Rector\SwissKnife\ValueObject\ClassConstantFetch\CurrentClassConstantFetch; use Rector\SwissKnife\ValueObject\ClassConstantFetch\ExternalClassAccessConstantFetch; +use Rector\SwissKnife\ValueObject\ClassConstantFetch\ParentClassConstantFetch; +use Rector\SwissKnife\ValueObject\ClassConstantFetch\StaticClassConstantFetch; use RuntimeException; final class ClassConstantFetchFinderTest extends AbstractTestCase @@ -45,6 +47,18 @@ public function test(): void $this->assertInstanceOf(ExternalClassAccessConstantFetch::class, $secondClassConstantFetch); } + public function testParentAndStaticFetches(): void + { + require_once __DIR__ . '/Fixture/ParentAndStatic/ParentClassWithConstant.php'; + + $classConstantFetches = $this->findInDirectory(__DIR__ . '/Fixture/ParentAndStatic'); + + $fetchTypes = array_map(static fn ($fetch) => $fetch::class, $classConstantFetches); + + $this->assertContains(ParentClassConstantFetch::class, $fetchTypes); + $this->assertContains(StaticClassConstantFetch::class, $fetchTypes); + } + public function testParseError(): void { $this->expectException(RuntimeException::class); @@ -61,11 +75,18 @@ public function testParseError(): void /** * @return ClassConstantFetchInterface[] */ - private function findInDirectory(string $directory): array + private function findInDirectory(string $directory, ?string $fileName = null): array { $progressBar = new ProgressBar(new OutputColorizer()); $fileInfos = PhpFilesFinder::find([$directory]); + if ($fileName !== null) { + $fileInfos = array_values(array_filter( + $fileInfos, + static fn ($fileInfo) => str_ends_with($fileInfo->getRealPath(), $fileName) + )); + } + return $this->classConstantFetchFinder->find($fileInfos, $progressBar, false); } } diff --git a/tests/PhpParser/Finder/ClassConstantFetchFinder/Fixture/ParentAndStatic/ClassWithParentAndStatic.php b/tests/PhpParser/Finder/ClassConstantFetchFinder/Fixture/ParentAndStatic/ClassWithParentAndStatic.php new file mode 100644 index 0000000000..1fc64b14fc --- /dev/null +++ b/tests/PhpParser/Finder/ClassConstantFetchFinder/Fixture/ParentAndStatic/ClassWithParentAndStatic.php @@ -0,0 +1,16 @@ +createStmts($extensionMethodCall); + + $this->assertCount(3, $stmts); + + $prettyPrinter = new \PhpParser\PrettyPrinter\Standard(); + $printed = $prettyPrinter->prettyPrintFile($stmts); + + $this->assertStringContainsString('strict_types=1', $printed); + $this->assertStringContainsString(ContainerConfigurator::class, $printed); + $this->assertStringContainsString(SymfonyClass::CONTAINER_CONFIGURATOR_CLASS, $printed); + } +} diff --git a/tests/PhpParser/NodeVisitor/AddImportConfigMethodCallNodeVisitorTest.php b/tests/PhpParser/NodeVisitor/AddImportConfigMethodCallNodeVisitorTest.php new file mode 100644 index 0000000000..b76070a3ab --- /dev/null +++ b/tests/PhpParser/NodeVisitor/AddImportConfigMethodCallNodeVisitorTest.php @@ -0,0 +1,30 @@ +params[] = new Param(new Variable('containerConfigurator'), null, new Identifier('ContainerConfigurator')); + + $addImportConfigMethodCallNodeVisitor = new AddImportConfigMethodCallNodeVisitor('/config/packages'); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor($addImportConfigMethodCallNodeVisitor); + + $nodeTraverser->traverse([$closure]); + + $this->assertCount(1, $closure->stmts); + } +} diff --git a/tests/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor/EntityClassNameCollectingNodeVisitorTest.php b/tests/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor/EntityClassNameCollectingNodeVisitorTest.php new file mode 100644 index 0000000000..5ffa32cc02 --- /dev/null +++ b/tests/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor/EntityClassNameCollectingNodeVisitorTest.php @@ -0,0 +1,53 @@ +addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $stmts = $parser->parse((string) file_get_contents($filePath)); + $this->assertNotNull($stmts); + + $nodeTraverser->traverse($stmts); + + $this->assertSame($expectedEntityClassNames, $visitor->getEntityClassNames()); + } + + /** + * @return iterable + */ + public static function provideData(): iterable + { + yield 'entity attribute' => [ + __DIR__ . '/Fixture/EntityWithAttribute.php', + ['Rector\SwissKnife\Tests\PhpParser\NodeVisitor\EntityClassNameCollectingNodeVisitor\Fixture\SomeEntity'], + ]; + + yield 'document annotation' => [ + __DIR__ . '/Fixture/DocumentWithAnnotation.php', + ['Rector\SwissKnife\Tests\PhpParser\NodeVisitor\EntityClassNameCollectingNodeVisitor\Fixture\SomeDocument'], + ]; + + yield 'non entity' => [__DIR__ . '/Fixture/RegularClass.php', []]; + } +} diff --git a/tests/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor/Fixture/DocumentWithAnnotation.php b/tests/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor/Fixture/DocumentWithAnnotation.php new file mode 100644 index 0000000000..5e7780ddce --- /dev/null +++ b/tests/PhpParser/NodeVisitor/EntityClassNameCollectingNodeVisitor/Fixture/DocumentWithAnnotation.php @@ -0,0 +1,12 @@ +addVisitor($extractSymfonyExtensionCallNodeVisitor); + + $stmts = $nodeTraverser->traverse([$extensionCall, $otherCall]); + + $this->assertCount(1, $stmts); + $this->assertCount(1, $extractSymfonyExtensionCallNodeVisitor->getExtensionMethodCalls()); + } +} diff --git a/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/ClassConstantFetchFinderDebugTest.php b/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/ClassConstantFetchFinderDebugTest.php new file mode 100644 index 0000000000..ce15edaee3 --- /dev/null +++ b/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/ClassConstantFetchFinderDebugTest.php @@ -0,0 +1,32 @@ +create(); + $finder = $container->make(ClassConstantFetchFinder::class); + + $fileInfos = PhpFilesFinder::find([ + __DIR__ . '/../../Finder/ClassConstantFetchFinder/Fixture/Standard', + ]); + + $progressBar = new ProgressBar(new OutputColorizer()); + $progressBar->start(1); + + $fetches = $finder->find($fileInfos, $progressBar, isDebug: true); + + $this->assertNotEmpty($fetches); + } +} diff --git a/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/FindClassConstFetchNodeVisitorExtendedTest.php b/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/FindClassConstFetchNodeVisitorExtendedTest.php new file mode 100644 index 0000000000..6a8f7fbe55 --- /dev/null +++ b/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/FindClassConstFetchNodeVisitorExtendedTest.php @@ -0,0 +1,151 @@ +addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $code = <<<'PHP' +traverse($this->parseCode($code)); + + $this->assertSame([], $visitor->getClassConstantFetches()); + } + + public function testSelfMagicClassConstantIsSkipped(): void + { + $visitor = new FindClassConstFetchNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $code = <<<'PHP' +traverse($this->parseCode($code)); + + $this->assertSame([], $visitor->getClassConstantFetches()); + } + + public function testDynamicConstantNameIsSkipped(): void + { + $visitor = new FindClassConstFetchNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $code = <<<'PHP' +traverse($this->parseCode($code)); + + $this->assertSame([], $visitor->getClassConstantFetches()); + } + + public function testCurrentClassConstant(): void + { + $visitor = new FindClassConstFetchNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $code = <<<'PHP' +traverse($this->parseCode($code)); + + $fetches = $visitor->getClassConstantFetches(); + $this->assertCount(1, $fetches); + $this->assertInstanceOf(CurrentClassConstantFetch::class, $fetches[0]); + } + + public function testInterfaceExists(): void + { + $visitor = new FindClassConstFetchNodeVisitor(); + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor(new NameResolver()); + $nodeTraverser->addVisitor($visitor); + + $code = <<<'PHP' +traverse($this->parseCode($code)); + + $this->assertSame([], $visitor->getClassConstantFetches()); + } + + /** + * @return array + */ + private function parseCode(string $code): array + { + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $nodes = $parser->parse($code); + $this->assertNotNull($nodes); + + return $nodes; + } +} diff --git a/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/FindClassConstFetchNodeVisitorTest.php b/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/FindClassConstFetchNodeVisitorTest.php new file mode 100644 index 0000000000..65ff7672e5 --- /dev/null +++ b/tests/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor/FindClassConstFetchNodeVisitorTest.php @@ -0,0 +1,100 @@ +createForNewestSupportedVersion(); + $code = <<<'PHP' +parse($code); + $this->assertNotNull($stmts); + + $nodeTraverser->traverse($stmts); + + $this->assertSame([], $visitor->getClassConstantFetches()); + } + + public function testExternalClassFetch(): void + { + require_once __DIR__ . '/../../Finder/ClassConstantFetchFinder/Fixture/Standard/AnotherClassWithConstant.php'; + + $visitor = new FindClassConstFetchNodeVisitor(); + $nodeTraverser = NodeTraverserFactory::create($visitor); + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $code = <<<'PHP' +parse($code); + $this->assertNotNull($stmts); + + $nodeTraverser->traverse($stmts); + + $fetches = $visitor->getClassConstantFetches(); + $this->assertCount(1, $fetches); + $this->assertInstanceOf(ExternalClassAccessConstantFetch::class, $fetches[0]); + } + + public function testNotImplementedYetException(): void + { + $this->expectException(NotImplementedYetException::class); + + $visitor = new FindClassConstFetchNodeVisitor(); + $nodeTraverser = NodeTraverserFactory::create($visitor); + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $code = <<<'PHP' +parse($code); + $this->assertNotNull($stmts); + + $nodeTraverser->traverse($stmts); + } +} diff --git a/tests/RobotLoader/PhpClassLoaderTest.php b/tests/RobotLoader/PhpClassLoaderTest.php new file mode 100644 index 0000000000..55ee541da0 --- /dev/null +++ b/tests/RobotLoader/PhpClassLoaderTest.php @@ -0,0 +1,30 @@ +load( + [__DIR__ . '/../Finder/MultiClassFixture'], + [] + ); + + $this->assertArrayHasKey( + 'Rector\SwissKnife\Tests\Finder\MultiClassFixture\FirstClassInFile', + $indexedClasses + ); + $this->assertArrayHasKey( + 'Rector\SwissKnife\Tests\Finder\MultiClassFixture\SecondClassInFile', + $indexedClasses + ); + } +} diff --git a/tests/SmokeTestgen/FileSystem/TestsDirectoryResolverTest.php b/tests/SmokeTestgen/FileSystem/TestsDirectoryResolverTest.php new file mode 100644 index 0000000000..8c54d0d0f6 --- /dev/null +++ b/tests/SmokeTestgen/FileSystem/TestsDirectoryResolverTest.php @@ -0,0 +1,43 @@ +tempDirectory = sys_get_temp_dir() . '/swiss-knife-tests-dir-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + } + + protected function tearDown(): void + { + FileSystem::delete($this->tempDirectory); + } + + public function testResolveWithUnitDirectory(): void + { + FileSystem::createDir($this->tempDirectory . '/tests/Unit'); + + $testsDirectoryResolver = new TestsDirectoryResolver(); + $smokeDirectory = $testsDirectoryResolver->resolveSmokeUnitTestDirectory($this->tempDirectory); + + $this->assertSame('tests/Unit/Smoke', $smokeDirectory); + } + + public function testResolveWithFallback(): void + { + $testsDirectoryResolver = new TestsDirectoryResolver(); + $smokeDirectory = $testsDirectoryResolver->resolveSmokeUnitTestDirectory($this->tempDirectory); + + $this->assertSame('tests/Unit/Smoke', $smokeDirectory); + } +} diff --git a/tests/SmokeTestgen/Templating/TemplateDecoratorFallbackTest.php b/tests/SmokeTestgen/Templating/TemplateDecoratorFallbackTest.php new file mode 100644 index 0000000000..3b2cdbff7d --- /dev/null +++ b/tests/SmokeTestgen/Templating/TemplateDecoratorFallbackTest.php @@ -0,0 +1,41 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-template-fallback-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testFallbackNamespaceWithoutComposerJson(): void + { + $templateDecorator = new TemplateDecorator(); + $decorated = $templateDecorator->decorate('__KERNEL_CLASS_PLACEHOLDER__ __SMOKE_TEST_NAMESPACE__'); + + $this->assertStringContainsString('App\\Tests\\Unit\\Smoke', $decorated); + $this->assertStringContainsString('Kernel', $decorated); + } +} diff --git a/tests/SmokeTestgen/Templating/TemplateDecoratorTest.php b/tests/SmokeTestgen/Templating/TemplateDecoratorTest.php new file mode 100644 index 0000000000..1fc4ddfe16 --- /dev/null +++ b/tests/SmokeTestgen/Templating/TemplateDecoratorTest.php @@ -0,0 +1,53 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-template-decorator-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testDecorate(): void + { + FileSystem::write($this->tempDirectory . '/composer.json', Json::encode([ + 'autoload-dev' => [ + 'psr-4' => [ + 'App\\Tests\\' => 'tests/', + ], + ], + ]), null); + + $templateDecorator = new TemplateDecorator(); + $decorated = $templateDecorator->decorate( + 'namespace __SMOKE_TEST_NAMESPACE__; class __KERNEL_CLASS_PLACEHOLDER__Test {}' + ); + + $this->assertStringContainsString('App\\Tests\\Unit\\Smoke', $decorated); + $this->assertStringNotContainsString('__SMOKE_TEST_NAMESPACE__', $decorated); + $this->assertStringNotContainsString('__KERNEL_CLASS_PLACEHOLDER__', $decorated); + } +} diff --git a/tests/SmokeTestgen/TestByPackageSubscriber/ServiceContainerTestByPackageSubscriberTest.php b/tests/SmokeTestgen/TestByPackageSubscriber/ServiceContainerTestByPackageSubscriberTest.php new file mode 100644 index 0000000000..969b496cf0 --- /dev/null +++ b/tests/SmokeTestgen/TestByPackageSubscriber/ServiceContainerTestByPackageSubscriberTest.php @@ -0,0 +1,22 @@ +assertSame( + ['symfony/symfony', 'symfony/dependency-injection'], + $subscriber->getPackageNames() + ); + $this->assertFileExists($subscriber->getTemplateFilePath()); + } +} diff --git a/tests/Testing/Finder/TestCaseClassFinderTest.php b/tests/Testing/Finder/TestCaseClassFinderTest.php new file mode 100644 index 0000000000..4cb4e1019f --- /dev/null +++ b/tests/Testing/Finder/TestCaseClassFinderTest.php @@ -0,0 +1,43 @@ +assertTrue(true); + } +} +PHP, null); + + $testCaseClassFinder = new TestCaseClassFinder(); + $classes = $testCaseClassFinder->findInDirectories([$tempDirectory]); + + $this->assertArrayHasKey('TempTests\SomeUnitTest', $classes); + + FileSystem::delete($tempDirectory); + } +} diff --git a/tests/Testing/MockWire/Fixture/ClassWithUntypedParameter.php b/tests/Testing/MockWire/Fixture/ClassWithUntypedParameter.php new file mode 100644 index 0000000000..ea9fdc06bb --- /dev/null +++ b/tests/Testing/MockWire/Fixture/ClassWithUntypedParameter.php @@ -0,0 +1,12 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Class "NonExistingClass" used in'); + + /** @phpstan-ignore argument.type (intentionally invalid class name) */ + MockWire::create('NonExistingClass', [new \stdClass()]); + } + + public function testNonObjectDependency(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('All constructor dependencies must be objects'); + + /** @phpstan-ignore argument.type (intentionally non-object dependency) */ + MockWire::create(ClassWithConstructorDependencies::class, ['not-an-object']); + } + + public function testEmptyDependencies(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('empty arguments'); + + MockWire::create(ClassWithConstructorDependencies::class, []); + } + + public function testNoConstructor(): void + { + $object = MockWire::create(SecondDependency::class, [new \stdClass()]); + + $this->assertInstanceOf(SecondDependency::class, $object); + } + + public function testUntypedParameter(): void + { + require_once __DIR__ . '/Fixture/ClassWithUntypedParameter.php'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only typed parameters can be automocked'); + + MockWire::create( + \Rector\SwissKnife\Tests\Testing\MockWire\Fixture\ClassWithUntypedParameter::class, + [new \stdClass()] + ); + } +} diff --git a/tests/Testing/PHPUnitMockerTest.php b/tests/Testing/PHPUnitMockerTest.php new file mode 100644 index 0000000000..801cbdd6f7 --- /dev/null +++ b/tests/Testing/PHPUnitMockerTest.php @@ -0,0 +1,20 @@ +create(\stdClass::class); + + $this->assertInstanceOf(MockObject::class, $mock); + } +} diff --git a/tests/Testing/Printer/PHPUnitXmlPrinterTest.php b/tests/Testing/Printer/PHPUnitXmlPrinterTest.php new file mode 100644 index 0000000000..21af4839fb --- /dev/null +++ b/tests/Testing/Printer/PHPUnitXmlPrinterTest.php @@ -0,0 +1,43 @@ +originalCwd = $originalCwd === false ? __DIR__ : $originalCwd; + + $this->tempDirectory = sys_get_temp_dir() . '/swiss-knife-xml-printer-' . uniqid(); + FileSystem::createDir($this->tempDirectory); + chdir($this->tempDirectory); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + FileSystem::delete($this->tempDirectory); + } + + public function testPrintFiles(): void + { + $filePath = $this->tempDirectory . '/SomeTest.php'; + FileSystem::write($filePath, 'printFiles([$filePath]); + + $this->assertSame('SomeTest.php' . PHP_EOL, $output); + } +} diff --git a/tests/Testing/UnitTestFilterExtendedTest.php b/tests/Testing/UnitTestFilterExtendedTest.php new file mode 100644 index 0000000000..7287bf06b0 --- /dev/null +++ b/tests/Testing/UnitTestFilterExtendedTest.php @@ -0,0 +1,28 @@ +make(UnitTestFilter::class); + + $testClassesToFilePaths = [ + 'Symfony\Component\Form\Test\TypeTestCase' => '/some/path/TypeTestCase.php', + 'Rector\SwissKnife\Tests\AbstractTestCase' => __DIR__ . '/../AbstractTestCase.php', + ]; + + $filtered = $unitTestFilter->filter($testClassesToFilePaths); + + $this->assertSame( + ['Rector\SwissKnife\Tests\AbstractTestCase' => __DIR__ . '/../AbstractTestCase.php'], + $filtered + ); + } +} diff --git a/tests/Testing/UnitTestFilterTest.php b/tests/Testing/UnitTestFilterTest.php new file mode 100644 index 0000000000..b1230fdb83 --- /dev/null +++ b/tests/Testing/UnitTestFilterTest.php @@ -0,0 +1,30 @@ +make(UnitTestFilter::class); + + $testClassesToFilePaths = [ + 'Rector\SwissKnife\Tests\AbstractTestCase' => __DIR__ . '/../AbstractTestCase.php', + 'Symfony\Bundle\FrameworkBundle\Test\KernelTestCase' => '/some/path/KernelTestCase.php', + 'NotATestClass' => '/some/path/NotATestClass.php', + ]; + + $filtered = $unitTestFilter->filter($testClassesToFilePaths); + + $this->assertSame( + ['Rector\SwissKnife\Tests\AbstractTestCase' => __DIR__ . '/../AbstractTestCase.php'], + $filtered + ); + } +} diff --git a/tests/ValueObject/ClassConstantFetch/AbstractClassConstantFetchTest.php b/tests/ValueObject/ClassConstantFetch/AbstractClassConstantFetchTest.php new file mode 100644 index 0000000000..4b49088f36 --- /dev/null +++ b/tests/ValueObject/ClassConstantFetch/AbstractClassConstantFetchTest.php @@ -0,0 +1,51 @@ +assertSame('SomeClass', $fetch->getClassName()); + $this->assertSame('FOO', $fetch->getConstantName()); + + $this->assertTrue($fetch->isClassConstantMatch(new ClassConstant('SomeClass', 'FOO'))); + $this->assertFalse($fetch->isClassConstantMatch(new ClassConstant('OtherClass', 'FOO'))); + $this->assertFalse($fetch->isClassConstantMatch(new ClassConstant('SomeClass', 'BAR'))); + } + + public function testExternalClassAccessConstantFetch(): void + { + $fetch = new ExternalClassAccessConstantFetch('ExternalClass', 'BAR'); + $this->assertTrue($fetch->isClassConstantMatch(new ClassConstant('ExternalClass', 'BAR'))); + $this->assertFalse($fetch->isClassConstantMatch(new ClassConstant('ExternalClass', 'BAZ'))); + } + + public function testParentClassConstantFetch(): void + { + $fetch = new ParentClassConstantFetch('ChildClass', 'PARENT_CONST'); + $this->assertSame('ChildClass', $fetch->getClassName()); + $this->assertSame('PARENT_CONST', $fetch->getConstantName()); + $this->assertTrue($fetch->isClassConstantMatch(new ClassConstant('ChildClass', 'PARENT_CONST'))); + $this->assertFalse($fetch->isClassConstantMatch(new ClassConstant('ParentClass', 'PARENT_CONST'))); + } + + public function testStaticClassConstantFetch(): void + { + $fetch = new StaticClassConstantFetch('SomeClass', 'STATIC_CONST'); + $this->assertSame('SomeClass', $fetch->getClassName()); + $this->assertSame('STATIC_CONST', $fetch->getConstantName()); + $this->assertTrue($fetch->isClassConstantMatch(new ClassConstant('SomeClass', 'STATIC_CONST'))); + $this->assertFalse($fetch->isClassConstantMatch(new ClassConstant('SomeClass', 'OTHER'))); + } +} diff --git a/tests/ValueObject/ClassConstantTest.php b/tests/ValueObject/ClassConstantTest.php new file mode 100644 index 0000000000..c3886af17b --- /dev/null +++ b/tests/ValueObject/ClassConstantTest.php @@ -0,0 +1,32 @@ +assertSame('SomeClass', $classConstant->getClassName()); + $this->assertSame('SOME_CONSTANT', $classConstant->getConstantName()); + } + + public function testEmptyClassName(): void + { + $this->expectException(InvalidArgumentException::class); + new ClassConstant('', 'SOME_CONSTANT'); + } + + public function testEmptyConstantName(): void + { + $this->expectException(InvalidArgumentException::class); + new ClassConstant('SomeClass', ''); + } +} diff --git a/tests/ValueObject/Traits/TraitSpottingResultTest.php b/tests/ValueObject/Traits/TraitSpottingResultTest.php new file mode 100644 index 0000000000..e8b9a48a0b --- /dev/null +++ b/tests/ValueObject/Traits/TraitSpottingResultTest.php @@ -0,0 +1,40 @@ +markUsedIn(__DIR__ . '/../../Traits/Fixture/TraitUser.php'); + + $secondTraitMetadata = new TraitMetadata(__DIR__ . '/../../Traits/Fixture/AnotherTrait.php', 'AnotherTrait'); + + $traitSpottingResult = new TraitSpottingResult([$firstTraitMetadata, $secondTraitMetadata]); + + $this->assertSame(2, $traitSpottingResult->getTraitCount()); + + $traitFilePaths = $traitSpottingResult->getTraitFilePaths(); + $this->assertCount(2, $traitFilePaths); + $this->assertSame( + [ + __DIR__ . '/../../Traits/Fixture/AnotherTrait.php', + __DIR__ . '/../../Traits/Fixture/SomeTrait.php', + ], + $traitFilePaths + ); + + $lazyTraits = $traitSpottingResult->getTraitMaximumUsedTimes(1); + $this->assertCount(1, $lazyTraits); + $this->assertSame('SomeTrait', $lazyTraits[0]->getShortTraitName()); + + $this->assertSame([], $traitSpottingResult->getTraitMaximumUsedTimes(0)); + } +} diff --git a/tests/ValueObject/VisibilityChangeStatsTest.php b/tests/ValueObject/VisibilityChangeStatsTest.php new file mode 100644 index 0000000000..95c79ef31b --- /dev/null +++ b/tests/ValueObject/VisibilityChangeStatsTest.php @@ -0,0 +1,30 @@ +assertFalse($visibilityChangeStats->hasAnyChange()); + $this->assertSame(0, $visibilityChangeStats->getPrivateCount()); + + $visibilityChangeStats->countPrivate(); + $visibilityChangeStats->countPrivate(); + + $this->assertTrue($visibilityChangeStats->hasAnyChange()); + $this->assertSame(2, $visibilityChangeStats->getPrivateCount()); + + $otherStats = new VisibilityChangeStats(); + $otherStats->countPrivate(); + + $visibilityChangeStats->merge($otherStats); + $this->assertSame(3, $visibilityChangeStats->getPrivateCount()); + } +}