mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 06:13:35 +02:00
wip
This commit is contained in:
+73
-9
@@ -98,6 +98,21 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
|
||||
private const string PIGGYBACK_COVERAGE_GLOBAL = 'TIA_PIGGYBACK_COVERAGE';
|
||||
|
||||
/**
|
||||
* The parent's resolved fallback branch, handed to the workers.
|
||||
*
|
||||
* A worker cannot resolve it for itself: the restarters run before
|
||||
* `tests/Pest.php` is loaded, so a `defaultBranch()` declared there is
|
||||
* invisible to it — and autodetecting again would spend a git call per
|
||||
* worker to reach the answer the parent already has.
|
||||
*/
|
||||
private const string FALLBACK_BRANCH_GLOBAL = 'TIA_FALLBACK_BRANCH';
|
||||
|
||||
/**
|
||||
* The branch assumed when a repository cannot name its own default.
|
||||
*/
|
||||
private const string DEFAULT_BRANCH = 'main';
|
||||
|
||||
/**
|
||||
* PHPUnit/Pest CLI flags whose subsequent argument is a value, not a path.
|
||||
*
|
||||
@@ -197,13 +212,22 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
/**
|
||||
* The baseline this run reads from and writes to.
|
||||
*
|
||||
* `main` is only the fallback for a repository whose branch cannot be read
|
||||
* — it is also the branch every other baseline falls back to reading, so
|
||||
* writing there by accident corrupts the shared baseline. Resolved through
|
||||
* resolveBranch() rather than at every use site, because the git call it
|
||||
* needs is not free.
|
||||
* The repository's default branch is only the fallback for a checkout whose
|
||||
* branch cannot be read — a detached HEAD. It is also the branch every
|
||||
* other baseline falls back to reading, so writing there by accident
|
||||
* corrupts the shared baseline. Resolved through resolveBranch() rather
|
||||
* than at every use site, because the git call it needs is not free.
|
||||
*/
|
||||
private string $branch = 'main';
|
||||
private string $branch = self::DEFAULT_BRANCH;
|
||||
|
||||
/**
|
||||
* The baseline branches with none of their own read from.
|
||||
*
|
||||
* Read-only, and the whole point of the exercise: without it the first run
|
||||
* on every new branch re-runs a suite whose results the default branch
|
||||
* already holds.
|
||||
*/
|
||||
private string $fallbackBranch = self::DEFAULT_BRANCH;
|
||||
|
||||
private bool $branchResolved = false;
|
||||
|
||||
@@ -289,7 +313,13 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
return null;
|
||||
}
|
||||
|
||||
return Graph::decode($json, $projectRoot);
|
||||
$graph = Graph::decode($json, $projectRoot);
|
||||
|
||||
// Every read of a baseline goes through a graph loaded here, so this is
|
||||
// the one place the resolved fallback has to reach.
|
||||
$graph?->setFallbackBranch($this->fallbackBranch);
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
private function saveGraph(Graph $graph): bool
|
||||
@@ -1724,6 +1754,11 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
// fallback baseline is the lesser of the two evils.
|
||||
}
|
||||
|
||||
// The graph above was loaded before the branch was known — this path
|
||||
// only writes, but a graph carrying an unresolved fallback is the exact
|
||||
// bug this whole change is about.
|
||||
$graph->setFallbackBranch($this->fallbackBranch);
|
||||
|
||||
$touchedFiles = [];
|
||||
|
||||
// Whether this run is the one that records the edges its results will be
|
||||
@@ -1892,7 +1927,7 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the baseline this run reads from and writes to, once.
|
||||
* Resolves the baselines this run reads from and writes to, once.
|
||||
*
|
||||
* Results are written on runs where TIA itself took no part, and those
|
||||
* never reach handleParent(). Without this the default would stand and
|
||||
@@ -1907,7 +1942,36 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
|
||||
$this->branchResolved = true;
|
||||
|
||||
$this->branch = new ChangedFiles($projectRoot)->currentBranch() ?? $this->branch;
|
||||
$changedFiles = new ChangedFiles($projectRoot);
|
||||
|
||||
// Resolved before the current branch, which throws where git is
|
||||
// missing: the fallback is advisory, so a run that cannot name its
|
||||
// branch at all should still carry the best answer available.
|
||||
$this->fallbackBranch = $this->resolveFallbackBranch($changedFiles);
|
||||
|
||||
Parallel::setGlobal(self::FALLBACK_BRANCH_GLOBAL, $this->fallbackBranch);
|
||||
|
||||
// A detached HEAD has no branch of its own to write to. The default
|
||||
// branch is the honest key there — it is the commit the checkout most
|
||||
// likely sits on, and it keeps a phantom baseline from being minted
|
||||
// under a branch name the repository never had.
|
||||
$this->branch = $changedFiles->currentBranch() ?? $this->fallbackBranch;
|
||||
}
|
||||
|
||||
private function resolveFallbackBranch(ChangedFiles $changedFiles): string
|
||||
{
|
||||
$inherited = Parallel::getGlobal(self::FALLBACK_BRANCH_GLOBAL);
|
||||
|
||||
if (is_string($inherited) && $inherited !== '') {
|
||||
return $inherited;
|
||||
}
|
||||
|
||||
// Configuration wins over autodetection: it is the escape hatch for a
|
||||
// repository whose `origin/HEAD` is unset and whose `init.defaultBranch`
|
||||
// says something else than its branches do.
|
||||
return $this->watchPatterns->defaultBranch()
|
||||
?? $changedFiles->defaultBranch()
|
||||
?? self::DEFAULT_BRANCH;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -219,6 +219,49 @@ final readonly class ChangedFiles
|
||||
return $branch === '' || $branch === 'HEAD' ? null : $branch;
|
||||
}
|
||||
|
||||
/**
|
||||
* The repository's default branch — the one every other branch's baseline
|
||||
* falls back to reading.
|
||||
*
|
||||
* Advisory, unlike {@see self::currentBranch()}: a repository that cannot
|
||||
* answer the question is not a broken repository. A remote-less checkout
|
||||
* has no `origin/HEAD`, and plenty of CI checkouts never run
|
||||
* `git remote set-head`, so every step here fails soft and the caller is
|
||||
* left to pick its own default.
|
||||
*/
|
||||
public function defaultBranch(): ?string
|
||||
{
|
||||
$head = $this->gitOutput(['git', 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
||||
|
||||
if ($head !== null) {
|
||||
$branch = preg_replace('#^origin/#', '', $head);
|
||||
|
||||
if (is_string($branch) && $branch !== '') {
|
||||
return $branch;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->gitOutput(['git', 'config', '--get', 'init.defaultBranch']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $command
|
||||
*/
|
||||
private function gitOutput(array $command): ?string
|
||||
{
|
||||
$process = new Process($command, $this->projectRoot);
|
||||
$process->setTimeout(5.0);
|
||||
$process->run();
|
||||
|
||||
if (! $process->isSuccessful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$output = trim($process->getOutput());
|
||||
|
||||
return $output === '' ? null : $output;
|
||||
}
|
||||
|
||||
private function shaIsReachable(string $sha): bool
|
||||
{
|
||||
$process = new Process(
|
||||
|
||||
@@ -60,6 +60,24 @@ final class Configuration
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The branch whose baseline every other branch falls back to reading.
|
||||
*
|
||||
* Autodetected from the repository when left unset; declare it here when
|
||||
* the repository cannot answer for itself — no `origin/HEAD`, or an
|
||||
* `init.defaultBranch` that disagrees with reality.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function defaultBranch(string $branch): self
|
||||
{
|
||||
/** @var WatchPatterns $watchPatterns */
|
||||
$watchPatterns = Container::getInstance()->get(WatchPatterns::class);
|
||||
$watchPatterns->setDefaultBranch($branch);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $patterns glob → project-relative test dir
|
||||
* @return $this
|
||||
|
||||
@@ -48,6 +48,17 @@ final class Graph
|
||||
*/
|
||||
private array $baselines = [];
|
||||
|
||||
/**
|
||||
* The baseline a branch with none of its own reads from.
|
||||
*
|
||||
* Only ever read from: a branch writes to its own key, so a fallback that
|
||||
* leaked into the write path would corrupt the baseline every other branch
|
||||
* depends on. Resolved once per run by the plugin — see
|
||||
* {@see self::setFallbackBranch()} — because the git calls it takes are not
|
||||
* free and the read path runs per test.
|
||||
*/
|
||||
private string $fallbackBranch = 'main';
|
||||
|
||||
private readonly string $projectRoot;
|
||||
|
||||
/** @var array<string, true>|null */
|
||||
@@ -576,7 +587,12 @@ final class Graph
|
||||
return $this->fingerprint;
|
||||
}
|
||||
|
||||
public function recordedAtSha(string $branch, string $fallbackBranch = 'main'): ?string
|
||||
public function setFallbackBranch(string $branch): void
|
||||
{
|
||||
$this->fallbackBranch = $branch;
|
||||
}
|
||||
|
||||
public function recordedAtSha(string $branch, ?string $fallbackBranch = null): ?string
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
|
||||
@@ -611,7 +627,7 @@ final class Graph
|
||||
$this->baselines[$branch]['results'][$testId] = $entry;
|
||||
}
|
||||
|
||||
public function getAssertions(string $branch, string $testId, string $fallbackBranch = 'main'): ?int
|
||||
public function getAssertions(string $branch, string $testId, ?string $fallbackBranch = null): ?int
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
|
||||
@@ -622,7 +638,7 @@ final class Graph
|
||||
return $baseline['results'][$testId]['assertions'];
|
||||
}
|
||||
|
||||
public function getTime(string $branch, string $testId, string $fallbackBranch = 'main'): ?float
|
||||
public function getTime(string $branch, string $testId, ?string $fallbackBranch = null): ?float
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
|
||||
@@ -633,7 +649,7 @@ final class Graph
|
||||
return $baseline['results'][$testId]['time'];
|
||||
}
|
||||
|
||||
public function getResult(string $branch, string $testId, string $fallbackBranch = 'main'): ?TestStatus
|
||||
public function getResult(string $branch, string $testId, ?string $fallbackBranch = null): ?TestStatus
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
|
||||
@@ -660,7 +676,7 @@ final class Graph
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function testFilesToRerun(string $branch, string $fallbackBranch = 'main'): array
|
||||
public function testFilesToRerun(string $branch, ?string $fallbackBranch = null): array
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
$files = [];
|
||||
@@ -697,7 +713,7 @@ final class Graph
|
||||
* collects no tests, so the run reports green without ever re-running the
|
||||
* failure — and does so again on every subsequent invocation.
|
||||
*/
|
||||
public function hasUnlocatedTestsToRerun(string $branch, string $fallbackBranch = 'main'): bool
|
||||
public function hasUnlocatedTestsToRerun(string $branch, ?string $fallbackBranch = null): bool
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
|
||||
@@ -808,7 +824,7 @@ final class Graph
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function lastRunTree(string $branch, string $fallbackBranch = 'main'): array
|
||||
public function lastRunTree(string $branch, ?string $fallbackBranch = null): array
|
||||
{
|
||||
return $this->baselineFor($branch, $fallbackBranch)['tree'];
|
||||
}
|
||||
@@ -816,8 +832,10 @@ final class Graph
|
||||
/**
|
||||
* @return array{sha: ?string, tree: array<string, string>, results: array<string, array{status: int, message: string, time: float, assertions?: int, file?: string}>}
|
||||
*/
|
||||
private function baselineFor(string $branch, string $fallbackBranch): array
|
||||
private function baselineFor(string $branch, ?string $fallbackBranch): array
|
||||
{
|
||||
$fallbackBranch ??= $this->fallbackBranch;
|
||||
|
||||
if (isset($this->baselines[$branch])) {
|
||||
return $this->baselines[$branch];
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ final class WatchPatterns
|
||||
|
||||
private bool $baselined = false;
|
||||
|
||||
private ?string $defaultBranch = null;
|
||||
|
||||
public function useDefaults(string $projectRoot): void
|
||||
{
|
||||
$testPath = TestSuite::getInstance()->testPath;
|
||||
@@ -177,6 +179,16 @@ final class WatchPatterns
|
||||
return $this->baselined;
|
||||
}
|
||||
|
||||
public function setDefaultBranch(string $branch): void
|
||||
{
|
||||
$this->defaultBranch = $branch;
|
||||
}
|
||||
|
||||
public function defaultBranch(): ?string
|
||||
{
|
||||
return $this->defaultBranch;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->patterns = [];
|
||||
@@ -185,6 +197,7 @@ final class WatchPatterns
|
||||
$this->locally = false;
|
||||
$this->filtered = false;
|
||||
$this->baselined = false;
|
||||
$this->defaultBranch = null;
|
||||
}
|
||||
|
||||
private function keyMatches(string $key, string $file): bool
|
||||
|
||||
Reference in New Issue
Block a user