This commit is contained in:
nuno maduro
2026-08-05 19:58:57 +01:00
parent 9f3c4e1e82
commit d112582857
30 changed files with 3200 additions and 11 deletions
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use RuntimeException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class TiaRequiresRemote extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function __construct()
{
parent::__construct(
'Tia mode requires a repository with a remote, so the default branch every other branch falls back to reading can be resolved.',
);
}
public function render(OutputInterface $output): void
{
$output->writeln([
'',
' <fg=white;options=bold;bg=red> ERROR </> Tia mode requires a repository with a remote.',
'',
' Without one there is no way to tell which branch is the default — the branch',
' whose baseline every other branch falls back to reading — so the first run on',
' each new branch would silently re-run the whole suite.',
'',
' Add a remote, or name the branch yourself in <fg=yellow>tests/Pest.php</>:',
'',
' <fg=yellow>pest()->tia()->defaultBranch(\'master\');</>',
'',
]);
}
public function exitCode(): int
{
return 1;
}
}
+51 -7
View File
@@ -9,8 +9,10 @@ use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Contracts\Plugins\HandlesOriginalArguments;
use Pest\Contracts\Plugins\Terminable;
use Pest\Exceptions\InvalidOption;
use Pest\Exceptions\MissingDependency;
use Pest\Exceptions\NoAffectedTestsFound;
use Pest\Exceptions\TiaRequiresRemote;
use Pest\Exceptions\TiaRequiresRepositoryRoot;
use Pest\Panic;
use Pest\Plugins\Concerns\HandleArguments;
@@ -183,6 +185,19 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
'--assignee', '--issue', '--ticket', '--pr', '--pull-request',
];
/**
* Options that cannot be combined with Tia mode.
*
* `--covers` and `--uses` select on coverage metadata Tia does not model,
* so they resolve to no tests at all rather than to the ones the user meant.
* `--random-order-seed` exits non-zero on its own, with or without Tia.
* Either way the run cannot honour both things it was asked for, so it says
* so instead of silently dropping Tia and running something else.
*/
private const array UNSUPPORTED_OPTIONS = [
'--covers', '--uses', '--random-order-seed',
];
private bool $graphWritten = false;
private bool $replayRan = false;
@@ -506,6 +521,10 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
$cliEnabled = $this->hasArgument(self::OPTION, $arguments) || self::envFlagEnabled(self::ENV_TIA);
$alwaysEnabled = $watchPatterns->isEnabled()
&& (! $watchPatterns->isLocally() || Environment::name() === Environment::LOCAL);
if (! $isWorker && ! $disabled && ($cliEnabled || $alwaysEnabled)) {
$this->guardUnsupportedOptions($arguments);
}
$hasExplicitPath = $this->hasExplicitPathArgument($arguments);
$partial = ! $isWorker && ($hasExplicitPath || $this->hasPartialSelection($arguments));
$disabled = $disabled || $partial;
@@ -869,6 +888,14 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
$this->resolveBranch($projectRoot);
// After resolveBranch(), so a directory that is no repository at all
// still reports the missing git dependency rather than a missing remote.
// Skipped once the default branch is configured by hand: there is then
// nothing left for a remote to answer.
if ($this->watchPatterns->defaultBranch() === null && ! new ChangedFiles($projectRoot)->hasRemote()) {
Panic::with(new TiaRequiresRemote);
}
$fingerprint = Fingerprint::compute($projectRoot);
$this->startFingerprint = $fingerprint;
@@ -1870,13 +1897,7 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
return true;
}
foreach (self::COVERAGE_REPORT_FLAGS as $flag) {
if ($this->hasArgument($flag, $this->originalArguments)) {
return true;
}
}
return false;
return array_any(self::COVERAGE_REPORT_FLAGS, fn (string $flag): bool => $this->hasArgument($flag, $this->originalArguments));
}
/**
@@ -1891,6 +1912,29 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
return $coverage->coverage;
}
/**
* Panics when the run asks for Tia alongside an option Tia cannot honour.
*
* Checked against the original argv as well, because `bin/pest` consumes
* some of these itself before PHPUnit ever sees them.
*
* @param array<int, string> $arguments
*/
private function guardUnsupportedOptions(array $arguments): void
{
foreach (self::UNSUPPORTED_OPTIONS as $option) {
if (! $this->hasArgument($option, $arguments) && ! $this->hasArgument($option, $this->originalArguments)) {
continue;
}
Panic::with(new InvalidOption(sprintf(
'The [%s] option cannot be combined with [%s].',
$option,
self::OPTION,
)));
}
}
/**
* Whether a selection-narrowing flag was given, either among the arguments
* PHPUnit receives or — for the flags `bin/pest` consumes itself — among
+11
View File
@@ -244,6 +244,17 @@ final readonly class ChangedFiles
return $this->gitOutput(['git', 'config', '--get', 'init.defaultBranch']);
}
/**
* Whether the repository has any remote configured.
*
* Advisory like {@see self::defaultBranch()} — a `git` that cannot answer
* is reported as "no remote", and the caller decides what that means.
*/
public function hasRemote(): bool
{
return $this->gitOutput(['git', 'remote']) !== null;
}
/**
* @param array<int, string> $command
*/