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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 2 additions & 58 deletions src/Plugins/Tia.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
use Pest\Plugins\Tia\CiDefaultBranch;
use Pest\Plugins\Tia\Contracts\State;
use Pest\Plugins\Tia\CoverageCollector;
use Pest\Plugins\Tia\DatabaseTestTables;
use Pest\Plugins\Tia\Fingerprint;
use Pest\Plugins\Tia\Graph;
use Pest\Plugins\Tia\JsModuleGraph;
use Pest\Plugins\Tia\Recorder;
use Pest\Plugins\Tia\ResultCollector;
use Pest\Plugins\Tia\SourceScope;
use Pest\Plugins\Tia\Storage;
use Pest\Plugins\Tia\TableExtractor;
use Pest\Plugins\Tia\WatchPatterns;
use Pest\Support\Container;
use Pest\Support\Git;
Expand Down Expand Up @@ -604,11 +604,7 @@ public function terminate(): void
$perTestUsesDatabase = $recorder->perTestUsesDatabase();

if ($perTestUsesDatabase !== []) {
$perTestTables = $this->augmentDatabaseTestTables(
$perTestTables,
$perTestUsesDatabase,
$projectRoot,
);
$perTestTables = DatabaseTestTables::augment($perTestTables, $perTestUsesDatabase, $projectRoot);
}

if (Parallel::isWorker()) {
Expand Down Expand Up @@ -2209,58 +2205,6 @@ private function composerLockDelta(string $projectRoot, string $sha): string
return implode(', ', $changes);
}

/**
* @param array<string, array<int, string>> $perTestTables
* @param array<string, true> $perTestUsesDatabase
* @return array<string, array<int, string>>
*/
private function augmentDatabaseTestTables(array $perTestTables, array $perTestUsesDatabase, string $projectRoot): array
{
$migrationDir = rtrim($projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'database'.DIRECTORY_SEPARATOR.'migrations';

if (! is_dir($migrationDir)) {
return $perTestTables;
}

$allTables = [];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($migrationDir, \FilesystemIterator::SKIP_DOTS),
);

foreach ($iterator as $fileInfo) {
if (! $fileInfo->isFile()) {
continue;
}
if (! str_ends_with(strtolower((string) $fileInfo->getPathname()), '.php')) {
continue;
}

$content = @file_get_contents((string) $fileInfo->getPathname());

if ($content === false) {
continue;
}

foreach (TableExtractor::fromMigrationSource($content) as $table) {
$allTables[strtolower($table)] = true;
}
}

if ($allTables === []) {
return $perTestTables;
}

foreach (array_keys($perTestUsesDatabase) as $testFile) {
$existing = $perTestTables[$testFile] ?? [];
$merged = array_fill_keys($existing, true) + $allTables;
$names = array_keys($merged);
sort($names);
$perTestTables[$testFile] = $names;
}

return $perTestTables;
}

/**
* @return array<string, string> package name → version
*/
Expand Down
86 changes: 86 additions & 0 deletions src/Plugins/Tia/DatabaseTestTables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Pest\Plugins\Tia;

use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

/**
* @internal
*/
final class DatabaseTestTables
{
/**
* @param array<string, array<int, string>> $perTestTables
* @param array<string, true> $perTestUsesDatabase
* @return array<string, array<int, string>>
*/
public static function augment(array $perTestTables, array $perTestUsesDatabase, string $projectRoot): array
{
$untracked = array_filter(
array_keys($perTestUsesDatabase),
fn (string $testFile): bool => ($perTestTables[$testFile] ?? []) === [],
);

if ($untracked === []) {
return $perTestTables;
}

$allTables = self::declaredTables($projectRoot);

if ($allTables === []) {
return $perTestTables;
}

foreach ($untracked as $testFile) {
$perTestTables[$testFile] = $allTables;
}

return $perTestTables;
}

/**
* @return array<int, string>
*/
private static function declaredTables(string $projectRoot): array
{
$migrationDir = rtrim($projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'database'.DIRECTORY_SEPARATOR.'migrations';

if (! is_dir($migrationDir)) {
return [];
}

$tables = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($migrationDir, FilesystemIterator::SKIP_DOTS),
);

foreach ($iterator as $fileInfo) {
if (! $fileInfo->isFile()) {
continue;
}

if (! str_ends_with(strtolower((string) $fileInfo->getPathname()), '.php')) {
continue;
}

$content = @file_get_contents((string) $fileInfo->getPathname());

if ($content === false) {
continue;
}

foreach (TableExtractor::fromMigrationSource($content) as $table) {
$tables[strtolower($table)] = true;
}
}

$names = array_keys($tables);
sort($names);

return $names;
}
}
54 changes: 27 additions & 27 deletions src/Plugins/Tia/Graph.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public function affected(array $changedFiles): array

$affectedSet = [];

$unparseableMigrations = $this->applyMigrationChanges($migrationPaths, $affectedSet);
$unmatchedMigrations = $this->applyMigrationChanges($migrationPaths, $affectedSet);

[$globalFrontendRuntimeFiles, $preciselyHandledPages, $sharedFilesResolved]
= $this->applyInertiaChanges($nonMigrationPaths, $affectedSet);
Expand All @@ -117,7 +117,7 @@ public function affected(array $changedFiles): array

$this->applyWatchPatternFallback(
$nonMigrationPaths,
$unparseableMigrations,
$unmatchedMigrations,
$preciselyHandledPages,
$sharedFilesResolved,
$handledBlade,
Expand Down Expand Up @@ -176,48 +176,48 @@ private function partitionChangedPaths(array $changedFiles): array
/**
* @param list<string> $migrationPaths
* @param array<string, true> $affectedSet
* @return list<string> Unparseable migrations (caller treats as unknown-to-graph).
* @return list<string> Migrations that could not be matched by table (caller treats as unknown-to-graph).
*/
private function applyMigrationChanges(array $migrationPaths, array &$affectedSet): array
{
if ($this->testTables === []) {
return $migrationPaths;
}

$changedTables = [];
$unparseable = [];
$unmatched = [];

foreach ($migrationPaths as $rel) {
$tables = $this->tablesForMigration($rel);

if ($tables === []) {
$unparseable[] = $rel;

continue;
}

foreach ($tables as $table) {
$changedTables[$table] = true;
if ($tables === [] || ! $this->selectTestsUsingTables($tables, $affectedSet)) {
$unmatched[] = $rel;
}
}

if ($changedTables !== []) {
foreach ($this->testTables as $testFile => $tables) {
if (isset($affectedSet[$testFile])) {
continue;
}
return $unmatched;
}

foreach ($tables as $table) {
if (isset($changedTables[$table])) {
$affectedSet[$testFile] = true;
/**
* @param list<string> $changedTables
* @param array<string, true> $affectedSet
*/
private function selectTestsUsingTables(array $changedTables, array &$affectedSet): bool
{
$changed = array_fill_keys($changedTables, true);
$selected = false;

break;
}
foreach ($this->testTables as $testFile => $tables) {
foreach ($tables as $table) {
if (isset($changed[$table])) {
$affectedSet[$testFile] = true;
$selected = true;

break;
}
}
}

return $unparseable;
return $selected;
}

/**
Expand Down Expand Up @@ -624,21 +624,21 @@ private function livewireGeneratedHash(string $path): ?string

/**
* @param list<string> $nonMigrationPaths
* @param list<string> $unparseableMigrations
* @param list<string> $unmatchedMigrations
* @param array<string, true> $preciselyHandledPages
* @param array<string, true> $sharedFilesResolved
* @param array<string, true> $handledBlade
* @param array<string, true> $affectedSet
*/
private function applyWatchPatternFallback(
array $nonMigrationPaths,
array $unparseableMigrations,
array $unmatchedMigrations,
array $preciselyHandledPages,
array $sharedFilesResolved,
array $handledBlade,
array &$affectedSet,
): void {
$unknownToGraph = $unparseableMigrations;
$unknownToGraph = $unmatchedMigrations;

foreach ($nonMigrationPaths as $rel) {
if (isset($preciselyHandledPages[$rel])) {
Expand Down
77 changes: 77 additions & 0 deletions tests/Unit/Plugins/Tia/DatabaseTestTables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

use Pest\Plugins\Tia\DatabaseTestTables;

describe('augment()', function (): void {
beforeEach(function (): void {
$this->projectRoot = sys_get_temp_dir().'/pest-tia-db-tables-'.bin2hex(random_bytes(4));
mkdir($this->projectRoot.'/database/migrations', 0755, true);
file_put_contents(
$this->projectRoot.'/database/migrations/2024_01_01_000000_create_orders_table.php',
"<?php Schema::create('orders', function () {});",
);
file_put_contents(
$this->projectRoot.'/database/migrations/2024_01_02_000000_create_users_table.php',
"<?php Schema::create('users', function () {});",
);
});

afterEach(function (): void {
foreach (glob($this->projectRoot.'/database/migrations/*.php') ?: [] as $migration) {
unlink($migration);
}

@rmdir($this->projectRoot.'/database/migrations');
@rmdir($this->projectRoot.'/database');
@rmdir($this->projectRoot);
});

it('keeps the recorded tables of a database test that tracked its queries', function (): void {
$augmented = DatabaseTestTables::augment(
['tests/Feature/OrderTest.php' => ['orders']],
['tests/Feature/OrderTest.php' => true],
$this->projectRoot,
);

expect($augmented)->toBe(['tests/Feature/OrderTest.php' => ['orders']]);
});

it('links a database test with no recorded tables to every declared table', function (): void {
$augmented = DatabaseTestTables::augment(
['tests/Feature/OrderTest.php' => ['orders']],
['tests/Feature/OrderTest.php' => true, 'tests/Feature/BootTest.php' => true],
$this->projectRoot,
);

expect($augmented)->toBe([
'tests/Feature/OrderTest.php' => ['orders'],
'tests/Feature/BootTest.php' => ['orders', 'users'],
]);
});

it('leaves tests that do not use the database untouched', function (): void {
$augmented = DatabaseTestTables::augment(
['tests/Unit/MathTest.php' => []],
[],
$this->projectRoot,
);

expect($augmented)->toBe(['tests/Unit/MathTest.php' => []]);
});

it('does nothing when the migrations declare no tables', function (): void {
foreach (glob($this->projectRoot.'/database/migrations/*.php') ?: [] as $migration) {
unlink($migration);
}

$augmented = DatabaseTestTables::augment(
[],
['tests/Feature/BootTest.php' => true],
$this->projectRoot,
);

expect($augmented)->toBeEmpty();
});
});
26 changes: 26 additions & 0 deletions tests/Unit/Plugins/Tia/Graph.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,32 @@
expect($affected)->toBe(['tests/Feature/OrderTest.php']);
});

it('falls back to watch patterns when no recorded test touches the changed table', function (): void {
$this->watchPatterns->add(['database/migrations/**' => 'tests/Feature']);

$graph = new Graph($this->projectRoot);
$graph->link('tests/Feature/UserTest.php', 'app/Models/User.php');
$graph->replaceTestTables([
'tests/Feature/UserTest.php' => ['users'],
]);

$affected = $graph->affected(['database/migrations/2024_01_01_000000_create_orders_table.php']);

expect($affected)->toBe(['tests/Feature/UserTest.php']);
});

it('selects nothing for an unrelated migration when no watch pattern covers it', function (): void {
$graph = new Graph($this->projectRoot);
$graph->link('tests/Feature/UserTest.php', 'app/Models/User.php');
$graph->replaceTestTables([
'tests/Feature/UserTest.php' => ['users'],
]);

$affected = $graph->affected(['database/migrations/2024_01_01_000000_create_orders_table.php']);

expect($affected)->toBeEmpty();
});

it('falls back to watch patterns when no table usage was recorded at all', function (): void {
$this->watchPatterns->add(['database/migrations/**' => 'tests/Feature']);

Expand Down