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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions composer-dependency-analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions phpunit-tia.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

use Rector\Tests\Tia\FixtureResolver;

return [
'resolvers' => [FixtureResolver::class],
];
7 changes: 7 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
</testsuite>
</testsuites>

<!-- test impact analysis: only re-run tests hit by changed files, requires pcov/xdebug to record -->
<extensions>
<bootstrap class="JMac\Testing\PhpUnit\Tia\Extension">
<parameter name="storage" value="global"/>
</bootstrap>
</extensions>

<php>
<ini name="memory_limit" value="-1"/>
</php>
Expand Down
8 changes: 8 additions & 0 deletions src/Testing/PHPUnit/AbstractLazyTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@

namespace Rector\Testing\PHPUnit;

use JMac\Testing\PhpUnit\Tia\Traits\RunWithTia;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\Version;
use Rector\Config\RectorConfig;
use Rector\DependencyInjection\LazyContainerFactory;

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();
Expand Down
59 changes: 59 additions & 0 deletions tests/Tia/FixtureResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Tia;

use JMac\Testing\PhpUnit\Tia\Contracts\Resolver;

/**
* Test impact analysis maps a changed file to a test via the recorded coverage graph. Fixture files
* never appear in that graph: "*.php.inc" is not executed code, and "Source/" or "Expected/" files are
* loaded by reflection, not by the test itself. Without this resolver, a fixture-only change would look
* like it affects nothing and the test would be skipped as unaffected.
*
* Every changed file below a test directory is therefore mapped to the closest test class above it.
*/
final class FixtureResolver implements Resolver
{
/**
* @var string[]
*/
private const TEST_DIRECTORIES = ['tests/', 'rules-tests/', 'utils/phpstan/tests/'];

/**
* @return list<string>
*/
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;
}
}
86 changes: 86 additions & 0 deletions tests/Tia/FixtureResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Tia;

use Nette\Utils\FileSystem;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class FixtureResolverTest extends TestCase
{
private string $projectRoot;

private FixtureResolver $fixtureResolver;

protected function setUp(): void
{
$this->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', []];
}
}
Loading