From 86adaedbbdea5ad3c8a8a5fa31b048d6943c99e6 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Tue, 4 Aug 2026 23:30:03 +0100 Subject: [PATCH] fix: tia filtered --- src/Plugins/Tia.php | 106 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/src/Plugins/Tia.php b/src/Plugins/Tia.php index 9d593789..0006d05d 100644 --- a/src/Plugins/Tia.php +++ b/src/Plugins/Tia.php @@ -7,6 +7,7 @@ namespace Pest\Plugins; use NunoMaduro\Collision\Adapters\Phpunit\Printers\DefaultPrinter; use Pest\Contracts\Plugins\AddsOutput; use Pest\Contracts\Plugins\HandlesArguments; +use Pest\Contracts\Plugins\HandlesOriginalArguments; use Pest\Contracts\Plugins\Terminable; use Pest\Exceptions\NoAffectedTestsFound; use Pest\Exceptions\TiaRequiresRepositoryRoot; @@ -36,7 +37,7 @@ use Symfony\Component\Process\Process; /** * @internal */ -final class Tia implements AddsOutput, HandlesArguments, Terminable +final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArguments, Terminable { use HandleArguments; @@ -107,6 +108,29 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable '--compact', '--ci-build-id', '--min', ]; + /** + * Flags that narrow this run to a subset of the suite. + * + * Only user-supplied, per-run narrowing belongs here. A filter that is + * always in force — `` in phpunit.xml, or a plugin registering a + * test case filter from `boot()` — applies equally to the runs that build + * the baseline, so it does not make this run narrower than the baseline + * and must not disable baseline writes. + * + * `--shard` is rewritten to `--filter` before this plugin sees the + * arguments, so it is covered here too. The `bin/pest`-only flags are + * stripped from the handled arguments, so they are matched against the + * original argv instead. + * + * @var list + */ + private const array PARTIAL_SELECTION_FLAGS = [ + '--filter', '--exclude-filter', '--group', '--exclude-group', + '--covers', '--uses', '--testsuite', '--exclude-testsuite', '--test-suffix', + '--dirty', '--todo', '--todos', '--flaky', '--notes', + '--assignee', '--issue', '--ticket', '--pr', '--pull-request', + ]; + private bool $graphWritten = false; private bool $replayRan = false; @@ -142,6 +166,11 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable private bool $filteredMode = false; + private bool $writesSuppressed = false; + + /** @var array */ + private array $originalArguments = []; + private ?string $driftLabel = null; private ?string $driftDetails = null; @@ -317,6 +346,14 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable return $this->cachedAssertionsByTestId[$testId] ?? 0; } + /** + * {@inheritDoc} + */ + public function handleOriginalArguments(array $arguments): void + { + $this->originalArguments = $arguments; + } + /** * {@inheritDoc} */ @@ -339,9 +376,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $cliEnabled = $this->hasArgument(self::OPTION, $arguments) || self::envFlagEnabled(self::ENV_TIA); $alwaysEnabled = $watchPatterns->isEnabled() && (! $watchPatterns->isLocally() || Environment::name() === Environment::LOCAL); + $hasExplicitPath = $this->hasExplicitPathArgument($arguments); + $partial = ! $isWorker && ($hasExplicitPath || $this->hasPartialSelection($arguments)); + $disabled = $disabled || $partial; $enabled = ! $disabled && ($cliEnabled || $alwaysEnabled); $this->filteredMode = ($this->hasArgument(self::FILTERED_OPTION, $arguments) || self::envFlagEnabled(self::ENV_FILTERED) || $watchPatterns->isFiltered()) - && ! $this->hasExplicitPathArgument($arguments) + && ! $hasExplicitPath && ! $this->coverageReportActive(); $freshRequested = $this->hasArgument(self::FRESH_OPTION, $arguments); $this->forceRefetch = $this->hasArgument(self::REFETCH_OPTION, $arguments); @@ -355,6 +395,22 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $arguments = $this->popArgument(self::BASELINED_OPTION, $arguments); if ($disabled) { + if ($partial) { + // Test results are collected unconditionally, and addOutput() + // folds them into an existing graph even when TIA took no part + // in the run. Left alone that would prune the cached results of + // every sibling test the selection excluded, so the writes have + // to be suppressed explicitly rather than merely skipped. + // `--no-tia` deliberately keeps writing: it still runs the whole + // suite, so its results remain valid for the baseline. + $this->writesSuppressed = true; + + if ($cliEnabled || $freshRequested || $this->forceRefetch) { + $this->output->writeln(''); + $this->renderChild('TIA does not apply to partial runs — running the selected tests directly.'); + } + } + $this->forceRefetch = false; $this->filteredMode = false; $this->freshRebuild = false; @@ -392,6 +448,16 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $this->flushWorkerReplay(); } + // Only ever set for the parent — addOutput() returns early in workers, + // whose partials are ephemeral and only reach the baseline if the + // parent consumes them, which it no longer does. + if ($this->writesSuppressed) { + $this->recorder->reset(); + $this->coverageCollector->reset(); + + return; + } + $recorder = $this->recorder; if (! $this->recordingActive && ! $recorder->isActive()) { @@ -481,12 +547,25 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable return $exitCode; } + // `->only()` narrows the executed set exactly like `--filter` does, but + // is only knowable once the suite has been collected — too late to turn + // TIA off up front, so instead every write is suppressed here. Sampled + // in addOutput() because Only's lock file is already gone by the time + // terminate() runs (its plugin terminates first). + if (Only::isEnabled()) { + $this->writesSuppressed = true; + } + $this->reportMissingWorkerDrivers(); if (Parallel::isEnabled()) { $this->mergeWorkerReplayPartials(); } + if ($this->writesSuppressed) { + return $exitCode; + } + if ($this->replayRan) { $this->bumpRecordedSha(); } @@ -1528,6 +1607,29 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable return $coverage->coverage; } + /** + * Whether a selection-narrowing flag was given, either among the arguments + * PHPUnit receives or — for the flags `bin/pest` consumes itself — among + * the original argv. Explicit path arguments and `->only()` are detected + * separately. + * + * @param array $arguments + */ + private function hasPartialSelection(array $arguments): bool + { + foreach (self::PARTIAL_SELECTION_FLAGS as $flag) { + if ($this->hasArgument($flag, $arguments)) { + return true; + } + + if ($this->hasArgument($flag, $this->originalArguments)) { + return true; + } + } + + return false; + } + /** * @param array $arguments */