mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 14:23:34 +02:00
wip
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Fixtures\Tia\Project;
|
||||
|
||||
/**
|
||||
* Reading a baseline recorded on another branch.
|
||||
*
|
||||
* Without the default-branch fallback, the first `--tia` run on every new branch
|
||||
* re-runs a whole suite whose results the default branch already holds — once
|
||||
* per branch, forever, on any repository not named `main`.
|
||||
*
|
||||
* @see https://github.com/pestphp/pest/issues/1823
|
||||
*/
|
||||
afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
test('replays the default branch baseline on a new branch', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe())
|
||||
->and($result->exitCode)->toBe(0);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('replays whatever the default branch is called', function (string $defaultBranch): void {
|
||||
$project = Project::make($defaultBranch);
|
||||
$project->seed($defaultBranch);
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe());
|
||||
})->with(['main', 'master', 'trunk', 'develop'])->skipOnWindows();
|
||||
|
||||
test('replays on a second new branch too', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
$project->pest('--tia');
|
||||
|
||||
$project->git()->switchTo('master');
|
||||
$project->git()->switchTo('feature-y', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
// The toll is one full run per new branch. It must not come back for the
|
||||
// second branch either.
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('writes nothing on a second run on the same branch', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
$project->pest('--tia');
|
||||
|
||||
$project->snapshot();
|
||||
$result = $project->pest('--tia');
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($delta->writtenCount())->toBe(0, $delta->summary())
|
||||
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('replays on a branch whose name contains slashes', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature/x/y', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe())
|
||||
->and($project->branchKeys())->toContain('feature/x/y');
|
||||
})->skipOnWindows();
|
||||
|
||||
test('replays inside a worktree on a new branch', function (): void {
|
||||
$project = Project::make('master');
|
||||
$worktree = $project->worktree('feature-worktree');
|
||||
|
||||
// Seeded against the worktree rather than the main checkout: a worktree's
|
||||
// `.git` is a file, so `Storage::originIdentity()` cannot read the remote
|
||||
// from it and the worktree resolves a storage key of its own. That gap is
|
||||
// separate from the branch fallback, and it is the fallback this row is
|
||||
// about — the worktree is checked out on a branch the baseline does not
|
||||
// name, which is the scenario from the issue.
|
||||
$project->seedFor($worktree, 'master');
|
||||
|
||||
$result = $project->pestIn($worktree, '--tia');
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe());
|
||||
})->skipOnWindows();
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\Process\ExecutableFinder;
|
||||
use Tests\Fixtures\Tia\Project;
|
||||
|
||||
/**
|
||||
* How the default branch gets named: declared in `tests/Pest.php`, autodetected
|
||||
* from the repository, or not answerable at all.
|
||||
*/
|
||||
afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
test('a declared default branch beats autodetection', function (): void {
|
||||
// The repository autodetects `develop`, which holds no baseline. Only the
|
||||
// declaration in `tests/Pest.php` can reach the `master` one.
|
||||
$project = Project::make('develop', overlay: 'configured-default-branch');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a declared default branch that does not exist degrades to a full run', function (): void {
|
||||
$project = Project::make('master', overlay: 'unknown-default-branch');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
// Nothing to fall back to, so everything runs — and no baseline is minted
|
||||
// under the name that resolved to nothing.
|
||||
expect($result->uncached())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($project->branchKeys())->toBe(['master', 'feature-x']);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a repository with no remote is refused rather than silently re-run', function (): void {
|
||||
$project = Project::make('master');
|
||||
|
||||
$project->git()->removeOrigin();
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
// Nothing can name the default branch, so every new branch would re-run the
|
||||
// whole suite with no explanation. Saying so beats doing that quietly.
|
||||
expect($result->output)->toContain('Tia mode requires a repository with a remote.')
|
||||
->and($result->exitCode)->toBe(1, $result->describe());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a declared default branch stands in for a missing remote', function (): void {
|
||||
// The escape hatch the refusal above points at: with the branch named by
|
||||
// hand there is nothing left for a remote to answer.
|
||||
$project = Project::make('master', overlay: 'configured-default-branch');
|
||||
|
||||
$project->git()->removeOrigin();
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('tia still requires git', function (): void {
|
||||
$project = Project::withoutGit();
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
// The soft default-branch resolver runs before the branch is named, and it
|
||||
// must not swallow this.
|
||||
expect($result->output)->toContain('The feature "Tia mode" requires "git".')
|
||||
->and($result->exitCode)->not->toBe(0);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a plain run outside a repository creates no baseline', function (): void {
|
||||
$project = Project::withoutGit();
|
||||
|
||||
$result = $project->pest();
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
|
||||
->and($project->graphExists())->toBeFalse()
|
||||
->and($project->graphDir())->not->toBeDirectory();
|
||||
})->skipOnWindows();
|
||||
|
||||
test('the default branch is resolved once per run, not once per test', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$git = (new ExecutableFinder)->find('git');
|
||||
|
||||
expect($git)->not->toBeNull();
|
||||
|
||||
// A `git` first on `PATH` that records what it was asked before handing over
|
||||
// to the real one.
|
||||
$log = $project->path('git-calls.log');
|
||||
$project->write('shim/git', implode("\n", [
|
||||
'#!/bin/sh',
|
||||
'echo "$@" >> '.escapeshellarg($log),
|
||||
'exec '.escapeshellarg((string) $git).' "$@"',
|
||||
'',
|
||||
]));
|
||||
chmod($project->path('shim/git'), 0755);
|
||||
|
||||
$result = $project->pestWithEnvironment($project->path(), [
|
||||
'PATH' => $project->path('shim').':'.getenv('PATH'),
|
||||
], '--tia');
|
||||
|
||||
$calls = file_exists($log) ? explode("\n", trim((string) file_get_contents($log))) : [];
|
||||
$resolutions = array_filter($calls, fn (string $call): bool => str_contains($call, 'symbolic-ref')
|
||||
|| str_contains($call, 'init.defaultBranch'));
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($calls)->not->toBeEmpty('the git shim was never reached')
|
||||
->and($resolutions)->toHaveCount(1, implode("\n", $calls));
|
||||
})->skipOnWindows();
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Fixtures\Tia\Project;
|
||||
|
||||
/**
|
||||
* What the fallback is allowed to touch.
|
||||
*
|
||||
* Reading another branch's baseline must stay a read: writes belong to the
|
||||
* branch that ran, and a branch that has no name of its own must not mint one.
|
||||
*/
|
||||
afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
test('narrows to the affected tests on a new branch', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
// Semantic, not cosmetic: PHP is hashed at the AST level, so a comment
|
||||
// would not register as a change at all.
|
||||
$project->write('app/Calculator.php', <<<'PHP'
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fixture\App;
|
||||
|
||||
final class Calculator
|
||||
{
|
||||
public function add(int $a, int $b): int
|
||||
{
|
||||
return $a + $b;
|
||||
}
|
||||
|
||||
public function subtract(int $a, int $b): int
|
||||
{
|
||||
return $a - $b;
|
||||
}
|
||||
|
||||
public function multiply(int $a, int $b): int
|
||||
{
|
||||
return $a * $b;
|
||||
}
|
||||
}
|
||||
PHP);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
// Two of the three test files cover `Calculator`; the third replays from the
|
||||
// default branch's baseline.
|
||||
expect($result->affected())->toBe(4, $result->describe())
|
||||
->and($result->replayed())->toBe(2, $result->describe())
|
||||
->and($result->exitCode)->toBe(0, $result->describe());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('filtered mode reads the fallback too', function (): void {
|
||||
$project = Project::make('master');
|
||||
|
||||
// A failure cached on the default branch. Filtered mode asks the graph which
|
||||
// test files are due a re-run, and that read has to reach the fallback as
|
||||
// well: without it a new branch sees nothing to do, reports green, and never
|
||||
// re-runs the failure — on every subsequent invocation.
|
||||
$project->seed('master', failing: ['adds two numbers']);
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia', '--filtered');
|
||||
|
||||
// Selection is by file, so the failure's two siblings in `CalculatorTest`
|
||||
// come along; the other two test files stay out of the run entirely.
|
||||
expect($result->output)->toContain('from 1 previously unsuccessful test')
|
||||
->and($result->output)->not->toContain('No affected tests found')
|
||||
->and($result->tally())->toContain('2 passed');
|
||||
})->skipOnWindows();
|
||||
|
||||
test('filtered mode finds nothing to do on a clean green feature branch', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia', '--filtered');
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->output)->toContain('No affected tests found')
|
||||
->and($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($delta->isHardSuppressed())->toBeTrue($delta->summary());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a detached HEAD replays without minting a branch key', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->detach();
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
// A detached HEAD has no branch of its own. The default branch is the
|
||||
// honest key, and no phantom one appears beside it.
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe())
|
||||
->and($project->branchKeys())->toBe(['master']);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('the branch that ran gets its own key and the default branch keeps its baseline', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
$project->pest('--tia');
|
||||
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($project->branchKeys())->toBe(['master', 'feature-x'])
|
||||
->and($delta->baselineUntouched('master'))->toBeTrue($delta->summary())
|
||||
->and($delta->writtenCount())->toBe(0, $delta->summary());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('the fallback reaches parallel workers', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$result = $project->pest('--tia', '--parallel', '--processes=2');
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe());
|
||||
})->skipOnWindows();
|
||||
Reference in New Issue
Block a user