From 026658ac41f12a94f2952fa6907012eecd3e67b3 Mon Sep 17 00:00:00 2001 From: Kevin McKee Date: Fri, 31 Jul 2026 12:47:19 -0500 Subject: [PATCH] feat: allow subtracting a warm-up boot baseline from Tia dependency edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frameworks that boot inside every test's setUp() re-execute the same bootstrap lines in every per-test coverage window. In a Laravel app every service provider, route file, and (with Filament) every panel resource executes getPages()/getRelations() on every boot, so Tia links every test to every bootstrap-executed file — a change to any of ~100 resource classes reruns the entire suite, defeating test impact analysis. pest()->tia()->warmupUsing(fn () => ...) registers a callback that runs once per process under the coverage driver before the first test. Every line it executes becomes a baseline that is subtracted from each test's recorded coverage before dependency edges are derived; a file only becomes an edge when a test executes lines beyond the baseline. Measured on a production Laravel 12 + Filament 5 app (~100 resources): a pure enum unit test dropped from 146 recorded edges to 6, a dashboard feature test from 1018 to 27, and editing an unrelated Filament resource went from invalidating the whole suite to invalidating nothing, while changes to genuinely shared models still selected their real dependents. Co-Authored-By: Claude Fable 5 --- src/Plugins/Tia/Configuration.php | 30 ++++++ src/Plugins/Tia/Recorder.php | 101 +++++++++++++++++++- tests/Fixtures/Tia/SharedSource.php | 19 ++++ tests/Fixtures/Tia/TestOnlySource.php | 11 +++ tests/Fixtures/Tia/WarmupExecutedSource.php | 11 +++ tests/Unit/Plugins/Tia/Recorder.php | 92 ++++++++++++++++++ 6 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/Tia/SharedSource.php create mode 100644 tests/Fixtures/Tia/TestOnlySource.php create mode 100644 tests/Fixtures/Tia/WarmupExecutedSource.php diff --git a/src/Plugins/Tia/Configuration.php b/src/Plugins/Tia/Configuration.php index 617898018..b077f33fa 100644 --- a/src/Plugins/Tia/Configuration.php +++ b/src/Plugins/Tia/Configuration.php @@ -4,6 +4,7 @@ namespace Pest\Plugins\Tia; +use Closure; use Pest\Support\Container; /** @@ -60,6 +61,35 @@ public function baselined(): self return $this; } + /** + * Register a warm-up callback whose executed lines are treated as + * framework bootstrap noise: it runs once per process under the coverage + * driver before the first test, and every line it executes is excluded + * from each test's recorded dependency edges. + * + * Intended for frameworks that boot inside every test (e.g. Laravel): + * + * ```php + * pest()->tia()->warmupUsing(function (): void { + * $app = require __DIR__.'/../bootstrap/app.php'; + * $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + * + * // Undo global state the boot mutated (container, facades, + * // environment variables, error handlers) before returning. + * }); + * ``` + * + * @return $this + */ + public function warmupUsing(Closure $callback): self + { + /** @var Recorder $recorder */ + $recorder = Container::getInstance()->get(Recorder::class); + $recorder->warmupUsing($callback); + + return $this; + } + /** * @param array $patterns glob → project-relative test dir * @return $this diff --git a/src/Plugins/Tia/Recorder.php b/src/Plugins/Tia/Recorder.php index c9e71870a..b1692ffd4 100644 --- a/src/Plugins/Tia/Recorder.php +++ b/src/Plugins/Tia/Recorder.php @@ -4,6 +4,7 @@ namespace Pest\Plugins\Tia; +use Closure; use Pest\TestSuite; use ReflectionClass; @@ -44,6 +45,33 @@ final class Recorder private ?SourceScope $sourceScope = null; + private ?Closure $warmup = null; + + /** @var array>|null */ + private ?array $warmupBaseline = null; + + /** + * Register a callback whose executed lines form the "warm-up baseline": + * before the first test of the process, the callback runs under the + * coverage driver, and every line it executes is subtracted from each + * test's recorded coverage before dependency edges are derived. + * + * Frameworks that boot inside every test's setUp() re-execute the same + * bootstrap lines in every test window (service providers, route + * registration, per-panel resource registration, …), which otherwise links + * every test to every bootstrap-executed file. Booting the framework once + * in the callback removes those edges while keeping any coverage a test + * adds beyond the baseline. + * + * The callback is responsible for cleaning up global state it mutates + * (container instances, facades, environment variables, error handlers) — + * it runs in the test process, immediately before the first test. + */ + public function warmupUsing(?Closure $callback): void + { + $this->warmup = $callback; + } + public function activate(): void { $this->active = true; @@ -116,6 +144,10 @@ public function beginTest(string $className, string $methodName, string $fallbac return; } + if ($this->warmupBaseline === null) { + $this->warmupBaseline = $this->collectWarmupBaseline(); + } + if ($this->driver === 'pcov') { \pcov\clear(); \pcov\start(); @@ -126,6 +158,71 @@ public function beginTest(string $className, string $methodName, string $fallbac \xdebug_start_code_coverage(); } + /** + * Run the registered warm-up callback under the coverage driver and + * collect the lines it executes, scoped to project sources. + * + * @return array> + */ + private function collectWarmupBaseline(): array + { + if (! $this->warmup instanceof Closure) { + return []; + } + + $scope = $this->sourceScope(); + + if ($this->driver === 'pcov') { + \pcov\clear(); + \pcov\start(); + + ($this->warmup)(); + + \pcov\stop(); + + $filesToCollectCoverageFor = []; + + foreach (\pcov\waiting() as $file) { + if (is_string($file) && $scope->contains($file)) { + $filesToCollectCoverageFor[] = $file; + } + } + + /** @var array $data */ + $data = \pcov\collect(\pcov\inclusive, $filesToCollectCoverageFor); + } else { + \xdebug_start_code_coverage(); + + ($this->warmup)(); + + /** @var array $data */ + $data = \xdebug_get_code_coverage(); + \xdebug_stop_code_coverage(true); + + foreach (array_keys($data) as $file) { + if (! $scope->contains($file)) { + unset($data[$file]); + } + } + } + + $baseline = []; + + foreach ($data as $file => $lines) { + if (! is_array($lines)) { + continue; + } + + foreach ($lines as $line => $count) { + if (is_int($count) && $count > 0) { + $baseline[$file][$line] = true; + } + } + } + + return $baseline; + } + public function endTest(): void { if (! $this->active || $this->currentTestFile === null) { @@ -348,9 +445,10 @@ private function filesWithExecutedLines(array $data): array if (! is_array($lines)) { continue; } + $baseline = $this->warmupBaseline[$file] ?? []; $covered = []; foreach ($lines as $line => $count) { - if (is_int($count) && $count > 0) { + if (is_int($count) && $count > 0 && ! isset($baseline[$line])) { $covered[] = $line; } } @@ -387,5 +485,6 @@ public function reset(): void $this->sourceScope = null; $this->active = false; $this->captureCoverage = false; + $this->warmupBaseline = null; } } diff --git a/tests/Fixtures/Tia/SharedSource.php b/tests/Fixtures/Tia/SharedSource.php new file mode 100644 index 000000000..972892dac --- /dev/null +++ b/tests/Fixtures/Tia/SharedSource.php @@ -0,0 +1,19 @@ +perTestTables())->toBeEmpty(); }); }); + +describe('warmupUsing()', function (): void { + /* + * The driver must not only exist — it must be able to instrument the + * fixture files (pcov only instruments files under pcov.directory). + */ + $driverCanInstrumentFixtures = function (): bool { + $fixture = dirname(__DIR__, 3).'/Fixtures/Tia/WarmupExecutedSource.php'; + require_once $fixture; + + if (function_exists('pcov\start')) { + \pcov\clear(); + \pcov\start(); + tia_warmup_executed_source(); + \pcov\stop(); + + $collected = \pcov\collect(\pcov\inclusive, [realpath($fixture) ?: $fixture]); + + return $collected !== []; + } + + return function_exists('xdebug_start_code_coverage') && function_exists('xdebug_info') && in_array('coverage', (array) xdebug_info('mode'), true); + }; + + beforeEach(function (): void { + require_once dirname(__DIR__, 3).'/Fixtures/Tia/WarmupExecutedSource.php'; + require_once dirname(__DIR__, 3).'/Fixtures/Tia/TestOnlySource.php'; + require_once dirname(__DIR__, 3).'/Fixtures/Tia/SharedSource.php'; + }); + + it('excludes files whose executed lines are fully covered by the warm-up baseline', function (): void { + $recorder = new Recorder; + $recorder->warmupUsing(function (): void { + tia_warmup_executed_source(); + }); + $recorder->activate(); + + $recorder->beginTest('Some\Missing\TestClass', 'boot noise', '/project/tests/Feature/BootNoiseTest.php'); + tia_warmup_executed_source(); + tia_test_only_source(); + $recorder->endTest(); + + $files = array_map(basename(...), $recorder->perTestFiles()['/project/tests/Feature/BootNoiseTest.php'] ?? []); + + expect($files)->toContain('TestOnlySource.php') + ->not->toContain('WarmupExecutedSource.php'); + })->skipOnWindows()->skip(fn (): bool => ! $driverCanInstrumentFixtures(), 'requires pcov (with pcov.directory covering the test fixtures) or xdebug (coverage mode)'); + + it('keeps files where a test executes lines beyond the warm-up baseline', function (): void { + $recorder = new Recorder; + $recorder->warmupUsing(function (): void { + tia_shared_source_boot_path(); + }); + $recorder->activate(); + + $recorder->beginTest('Some\Missing\TestClass', 'beyond baseline', '/project/tests/Feature/BeyondBaselineTest.php'); + tia_shared_source_boot_path(); + tia_shared_source_test_path(); + $recorder->endTest(); + + $files = array_map(basename(...), $recorder->perTestFiles()['/project/tests/Feature/BeyondBaselineTest.php'] ?? []); + + expect($files)->toContain('SharedSource.php'); + })->skipOnWindows()->skip(fn (): bool => ! $driverCanInstrumentFixtures(), 'requires pcov (with pcov.directory covering the test fixtures) or xdebug (coverage mode)'); + + it('records unchanged edges when no warm-up is registered', function (): void { + $recorder = new Recorder; + $recorder->activate(); + + $recorder->beginTest('Some\Missing\TestClass', 'no warmup', '/project/tests/Feature/NoWarmupTest.php'); + tia_warmup_executed_source(); + $recorder->endTest(); + + $files = array_map(basename(...), $recorder->perTestFiles()['/project/tests/Feature/NoWarmupTest.php'] ?? []); + + expect($files)->toContain('WarmupExecutedSource.php'); + })->skipOnWindows()->skip(fn (): bool => ! $driverCanInstrumentFixtures(), 'requires pcov (with pcov.directory covering the test fixtures) or xdebug (coverage mode)'); + + it('does not invoke the warm-up callback without a coverage driver', function (): void { + $recorder = new Recorder; + $invoked = false; + $recorder->warmupUsing(function () use (&$invoked): void { + $invoked = true; + }); + $recorder->activateLinkTracking(); + + $recorder->beginTest('Some\Missing\TestClass', 'link tracking', '/project/tests/Feature/LinkTrackingTest.php'); + $recorder->endTest(); + + expect($invoked)->toBeFalse(); + }); +});