diff --git a/src/Plugins/Tia.php b/src/Plugins/Tia.php index 09928439..dc0df77f 100644 --- a/src/Plugins/Tia.php +++ b/src/Plugins/Tia.php @@ -562,7 +562,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable return $exitCode; } - $this->snapshotTestResults(); + $this->snapshotTestResults(markKnownTestFiles: true); return $exitCode; } @@ -1394,12 +1394,13 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable ); } + $graph->markKnownTestFiles(array_keys($touchedFiles)); $graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results)); $collector->reset(); } - private function snapshotTestResults(): void + private function snapshotTestResults(bool $markKnownTestFiles = false): void { /** @var ResultCollector $collector */ $collector = Container::getInstance()->get(ResultCollector::class); @@ -1442,6 +1443,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable ); } + if ($markKnownTestFiles) { + $graph->markKnownTestFiles(array_keys($touchedFiles)); + } + $graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results)); $this->saveGraph($graph); diff --git a/src/Plugins/Tia/Graph.php b/src/Plugins/Tia/Graph.php index a5c7a92a..1f72f67f 100644 --- a/src/Plugins/Tia/Graph.php +++ b/src/Plugins/Tia/Graph.php @@ -813,6 +813,38 @@ final class Graph } } + /** + * Mark test files that executed under a recorded coverage session as "known", + * seeding an empty edge set for any that produced zero project-source edges. + * + * Without this, a test that covers no application source (e.g. a pure unit + * test asserting on language primitives) never becomes an edge key, so + * {@see self::knowsTest()} reports it as unknown and it re-runs on every TIA + * run. Recording it with an empty edge set lets it be replayed/skipped while + * unchanged; it is still re-run whenever its own file changes, via + * {@see self::applyTestFileChanges()}. + * + * Must only be called from the recording path, where coverage was actually + * collected — otherwise a missing edge set could mean "coverage was off", + * not "genuinely covered nothing". + * + * @param array $testFiles Absolute or project-relative test file paths. + */ + public function markKnownTestFiles(array $testFiles): void + { + foreach ($testFiles as $testFile) { + $rel = $this->relative($testFile); + + if ($rel === null) { + continue; + } + + if (! isset($this->edges[$rel])) { + $this->edges[$rel] = []; + } + } + } + /** * @param array> $testToTables */ diff --git a/src/Plugins/Tia/TestPaths.php b/src/Plugins/Tia/TestPaths.php index 0b5308da..eb1fa7c4 100644 --- a/src/Plugins/Tia/TestPaths.php +++ b/src/Plugins/Tia/TestPaths.php @@ -45,9 +45,7 @@ final readonly class TestPaths $directories[] = $rel; } - $suffix = $directory->suffix(); - - $suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix; + $suffixes[] = $directory->suffix(); } foreach ($suite->files() as $file) { @@ -61,7 +59,7 @@ final readonly class TestPaths if ($suffixes === []) { foreach ($configuration->testSuffixes() as $suffix) { - $suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix; + $suffixes[] = $suffix; } } } catch (Throwable) { diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index b5d6918d..93fda4f2 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1821,6 +1821,11 @@ ✓ output format → it returns a 32-character hex xxh128 hash ✓ output format → it returns a stable hash for empty content + PASS Tests\Unit\Plugins\Tia\Graph + ✓ markKnownTestFiles() → it makes a test file with no edges known + ✓ markKnownTestFiles() → it does not clobber edges of an already-known test file + ✓ markKnownTestFiles() → it ignores paths outside the project root + PASS Tests\Unit\Plugins\Tia\IsEnabledForRun ✓ does not throw when an integer --random-order-seed is passed as a separate argv element ✓ still detects --tia when an integer argument is present @@ -1836,6 +1841,14 @@ ✓ it detects an added or removed non-platform dependency ✓ it is independent of package ordering + PASS Tests\Unit\Plugins\Tia\TestPaths + ✓ isTestFile() → it matches a standard test file under a configured directory + ✓ isTestFile() → it does not match a non-test file that shares the directory + ✓ isTestFile() → it does not match files outside the configured directories + ✓ isTestFile() → it matches an explicitly listed file + ✓ isTestFile() → it honours a bare .php suffix + ✓ isTestFile() → it does not require a dot before the suffix + PASS Tests\Unit\Plugins\Tia\ViteDepsHelper ✓ it strips JSONC down to something JSON.parse accepts with ('plain-object') ✓ it strips JSONC down to something JSON.parse accepts with ('plain-array') @@ -2084,4 +2097,4 @@ ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') - Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1462 passed (3220 assertions) \ No newline at end of file + Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1471 passed (3232 assertions) \ No newline at end of file diff --git a/tests/Unit/Plugins/Tia/Graph.php b/tests/Unit/Plugins/Tia/Graph.php new file mode 100644 index 00000000..786c4171 --- /dev/null +++ b/tests/Unit/Plugins/Tia/Graph.php @@ -0,0 +1,37 @@ +knowsTest('tests/Unit/ExampleTest.php'))->toBeFalse(); + + $graph->markKnownTestFiles(['tests/Unit/ExampleTest.php']); + + expect($graph->knowsTest('tests/Unit/ExampleTest.php'))->toBeTrue(); + }); + + it('does not clobber edges of an already-known test file', function (): void { + $graph = new Graph(sys_get_temp_dir()); + $graph->link('tests/Feature/UserTest.php', 'app/Models/User.php'); + + $graph->markKnownTestFiles(['tests/Feature/UserTest.php']); + + // The pre-existing edge survives — the test still depends on the source file. + $affected = $graph->affected(['app/Models/User.php']); + + expect($affected)->toContain('tests/Feature/UserTest.php'); + }); + + it('ignores paths outside the project root', function (): void { + $graph = new Graph(sys_get_temp_dir()); + + $graph->markKnownTestFiles(['/somewhere/else/tests/FooTest.php']); + + expect($graph->knowsTest('/somewhere/else/tests/FooTest.php'))->toBeFalse(); + }); +}); diff --git a/tests/Unit/Plugins/Tia/TestPaths.php b/tests/Unit/Plugins/Tia/TestPaths.php new file mode 100644 index 00000000..69190eb6 --- /dev/null +++ b/tests/Unit/Plugins/Tia/TestPaths.php @@ -0,0 +1,72 @@ +isTestFile('tests/Unit/ExampleTest.php'))->toBeTrue() + ->and($paths->isTestFile('tests/Feature/UserTest.php'))->toBeTrue(); + }); + + it('does not match a non-test file that shares the directory', function (): void { + $paths = new TestPaths( + directories: ['tests/Unit'], + files: [], + suffixes: ['Test.php'], + ); + + expect($paths->isTestFile('tests/Unit/Helper.php'))->toBeFalse(); + }); + + it('does not match files outside the configured directories', function (): void { + $paths = new TestPaths( + directories: ['tests/Unit'], + files: [], + suffixes: ['Test.php'], + ); + + expect($paths->isTestFile('app/Models/UserTest.php'))->toBeFalse(); + }); + + it('matches an explicitly listed file', function (): void { + $paths = new TestPaths( + directories: [], + files: ['tests/Pest.php'], + suffixes: ['Test.php'], + ); + + expect($paths->isTestFile('tests/Pest.php'))->toBeTrue(); + }); + + it('honours a bare .php suffix', function (): void { + $paths = new TestPaths( + directories: ['tests'], + files: [], + suffixes: ['.php'], + ); + + expect($paths->isTestFile('tests/Unit/ExampleTest.php'))->toBeTrue() + ->and($paths->isTestFile('tests/Unit/Helper.php'))->toBeTrue(); + }); + + // Regression: a suffix of "Test.php" must match "ExampleTest.php". A previous + // normalisation prepended a dot (".Test.php"), which matched nothing and left + // edited tests looking unchanged to TIA, replaying stale results. + it('does not require a dot before the suffix', function (): void { + $paths = new TestPaths( + directories: ['tests/Unit'], + files: [], + suffixes: ['.Test.php'], + ); + + expect($paths->isTestFile('tests/Unit/ExampleTest.php'))->toBeFalse(); + }); +}); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index e249974a..a59fc7f1 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -24,13 +24,13 @@ test('parallel', function () use ($run): void { $file = file_get_contents(__FILE__); $file = preg_replace( '/\$expected = \'.*?\';/', - "\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1445 passed (3167 assertions)';", + "\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1454 passed (3179 assertions)';", $file, ); file_put_contents(__FILE__, $file); } - $expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1445 passed (3167 assertions)'; + $expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1454 passed (3179 assertions)'; expect($output) ->toContain("Tests: {$expected}")