diff --git a/.gitattributes b/.gitattributes index 7c9ff784df6..12e0abc1839 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ tests/FileFormatter/ValueObject/Fixture/composer_carriage_return_line_feed.json # for 3rd party packages working with rector/rector-src as dependency rules-tests export-ignore tests export-ignore +phpunit-tia.php export-ignore diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 9b360269026..a104d836fcb 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -12,6 +12,7 @@ ->addPathToScan('bin', false) // prepared test tooling ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::DEV_DEPENDENCY_IN_PROD]) + ->ignoreErrorsOnPackage('jasonmccreary/phpunit-tia', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // pinned v3.x version ->ignoreErrorsOnPackage('react/promise', [ErrorType::UNUSED_DEPENDENCY]) // ensure use version ^3.2.0 diff --git a/composer.json b/composer.json index dd8901d5d78..5463ed743eb 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "webmozart/assert": "^2.4" }, "require-dev": { + "jasonmccreary/phpunit-tia": "^0.1.1", "nette/robot-loader": "^4.1", "php-parallel-lint/php-parallel-lint": "^1.4", "phpstan/extension-installer": "^1.4", diff --git a/phpunit-tia.php b/phpunit-tia.php new file mode 100644 index 00000000000..2bbd2584ed4 --- /dev/null +++ b/phpunit-tia.php @@ -0,0 +1,9 @@ + [FixtureResolver::class], +]; diff --git a/phpunit.xml b/phpunit.xml index 37c4eb12d7f..739b4ef8f26 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,6 +18,13 @@ + + + + + + + diff --git a/src/Testing/PHPUnit/AbstractLazyTestCase.php b/src/Testing/PHPUnit/AbstractLazyTestCase.php index 3fc41dd2e3a..11a13553d07 100644 --- a/src/Testing/PHPUnit/AbstractLazyTestCase.php +++ b/src/Testing/PHPUnit/AbstractLazyTestCase.php @@ -4,6 +4,7 @@ namespace Rector\Testing\PHPUnit; +use JMac\Testing\PhpUnit\Tia\Traits\RunWithTia; use PHPUnit\Framework\TestCase; use PHPUnit\Runner\Version; use Rector\Config\RectorConfig; @@ -11,10 +12,17 @@ abstract class AbstractLazyTestCase extends TestCase { + // skips tests unaffected by the changed files, based on the recorded coverage graph + use RunWithTia { + RunWithTia::setUp as tiaSetUp; + } + protected static ?RectorConfig $rectorConfig = null; protected function setUp(): void { + $this->tiaSetUp(); + // this is needed to have always the same preloaded nikic/php-parser classes // in both bare AbstractLazyTestCase lazy tests and AbstractRectorTestCase tests $this->includePreloadFilesAndScoperAutoload(); diff --git a/tests/Tia/FixtureResolver.php b/tests/Tia/FixtureResolver.php new file mode 100644 index 00000000000..994786dc7c0 --- /dev/null +++ b/tests/Tia/FixtureResolver.php @@ -0,0 +1,59 @@ + + */ + public function resolve(string $projectRoot, string $changedRelativePath): array + { + if (! $this->isInTestDirectory($changedRelativePath)) { + return []; + } + + $rootDirectory = rtrim(str_replace('\\', '/', $projectRoot), '/'); + $directory = dirname($rootDirectory . '/' . $changedRelativePath); + + while (str_starts_with($directory . '/', $rootDirectory . '/') && $directory !== $rootDirectory) { + $testFilePaths = glob($directory . '/*Test.php'); + + if ($testFilePaths !== false && $testFilePaths !== []) { + return array_values($testFilePaths); + } + + $directory = dirname($directory); + } + + return []; + } + + private function isInTestDirectory(string $changedRelativePath): bool + { + foreach (self::TEST_DIRECTORIES as $testDirectory) { + if (str_starts_with($changedRelativePath, $testDirectory)) { + return true; + } + } + + return false; + } +} diff --git a/tests/Tia/FixtureResolverTest.php b/tests/Tia/FixtureResolverTest.php new file mode 100644 index 00000000000..fb51bfcdd87 --- /dev/null +++ b/tests/Tia/FixtureResolverTest.php @@ -0,0 +1,86 @@ +projectRoot = sys_get_temp_dir() . '/rector_tia_fixture_resolver'; + + FileSystem::delete($this->projectRoot); + + foreach ([ + 'rules-tests/SomeSet/Rector/If_/SomeRector/SomeRectorTest.php', + 'rules-tests/SomeSet/Rector/If_/SomeRector/Fixture/some_fixture.php.inc', + 'rules-tests/SomeSet/Rector/If_/SomeRector/Fixture/nested/deep_fixture.php.inc', + 'rules-tests/SomeSet/Rector/If_/SomeRector/Source/SomeSource.php', + 'rules-tests/SomeSet/Rector/If_/WithoutTestRector/Fixture/orphan.php.inc', + 'src/SomeService.php', + ] as $relativeFilePath) { + FileSystem::write($this->projectRoot . '/' . $relativeFilePath, ''); + } + + $this->fixtureResolver = new FixtureResolver(); + } + + protected function tearDown(): void + { + FileSystem::delete($this->projectRoot); + } + + /** + * @param string[] $expectedRelativeFilePaths + */ + #[DataProvider('provideData')] + public function testResolve(string $changedRelativePath, array $expectedRelativeFilePaths): void + { + $resolvedFilePaths = $this->fixtureResolver->resolve($this->projectRoot, $changedRelativePath); + + $expectedFilePaths = array_map( + fn (string $relativeFilePath): string => $this->projectRoot . '/' . $relativeFilePath, + $expectedRelativeFilePaths + ); + + $this->assertSame($expectedFilePaths, $resolvedFilePaths); + } + + public static function provideData(): iterable + { + $testFilePath = 'rules-tests/SomeSet/Rector/If_/SomeRector/SomeRectorTest.php'; + + yield 'fixture file resolves to the sibling test' => [ + 'rules-tests/SomeSet/Rector/If_/SomeRector/Fixture/some_fixture.php.inc', + [$testFilePath], + ]; + + yield 'nested fixture file walks up to the test' => [ + 'rules-tests/SomeSet/Rector/If_/SomeRector/Fixture/nested/deep_fixture.php.inc', + [$testFilePath], + ]; + + yield 'source file resolves to the sibling test' => [ + 'rules-tests/SomeSet/Rector/If_/SomeRector/Source/SomeSource.php', + [$testFilePath], + ]; + + yield 'test file itself resolves to itself' => [$testFilePath, [$testFilePath]]; + + yield 'fixture without any test above it resolves to nothing' => [ + 'rules-tests/SomeSet/Rector/If_/WithoutTestRector/Fixture/orphan.php.inc', + [], + ]; + + yield 'file outside test directories is ignored' => ['src/SomeService.php', []]; + } +}