This commit is contained in:
nuno maduro
2026-08-06 17:52:04 +01:00
parent a40cc6bc0e
commit b795af3b1b
7 changed files with 509 additions and 22 deletions
+209
View File
@@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
use Tests\Fixtures\Tia\Project;
afterEach(function (): void {
Project::destroyAll();
});
test('a complete run prunes a deleted test', function (): void {
$project = Project::make('master');
$project->seed('master');
$project->write('tests/Unit/GreeterTest.php', <<<'PHP'
<?php
declare(strict_types=1);
use Fixture\App\Greeter;
test('greets a person', function (): void {
expect((new Greeter)->greet('Nuno'))->toBe('Hello, Nuno!');
});
PHP);
$result = $project->pest();
$delta = $project->delta();
expect($result->tally())->toContain('5 passed')
->and($delta->removed())->toBe(1, $delta->summary())
->and($delta->added())->toBe(0, $delta->summary())
->and($delta->structureMoved())->toBeFalse($delta->summary());
})->skipOnWindows();
test('a complete run records nothing for a test file the graph does not know', function (): void {
$project = Project::make('master');
$project->seed('master');
$project->write('tests/Unit/BrandNewTest.php', <<<'PHP'
<?php
declare(strict_types=1);
test('brand new thing', function (): void {
expect(true)->toBeTrue();
});
PHP);
$result = $project->pest();
$delta = $project->delta();
expect($result->tally())->toContain('7 passed')
->and($delta->added())->toBe(0, $delta->summary())
->and($delta->removed())->toBe(0, $delta->summary())
->and($delta->writtenCount())->toBe(Project::TOTAL_TESTS, $delta->summary());
})->skipOnWindows();
test('a partial run records nothing for a test file the graph does not know', function (): void {
$project = Project::make('master');
$project->seed('master');
$project->write('tests/Unit/BrandNewTest.php', <<<'PHP'
<?php
declare(strict_types=1);
test('brand new thing', function (): void {
expect(true)->toBeTrue();
});
PHP);
$result = $project->pest('--filter=brand new thing');
$delta = $project->delta();
expect($result->tally())->toContain('1 passed')
->and($delta->added())->toBe(0, $delta->summary())
->and($delta->writtenCount())->toBe(0, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('a truncated run does not prune', function (): void {
$project = Project::make('master');
$project->seed('master');
$project->write('tests/Unit/GreeterTest.php', <<<'PHP'
<?php
declare(strict_types=1);
use Fixture\App\Greeter;
test('greets a person', function (): void {
expect((new Greeter)->greet('Nuno'))->toBe('Goodbye, Nuno!');
});
test('greets the world', function (): void {
expect((new Greeter)->greet('world'))->toBe('Hello, world!');
});
PHP);
$result = $project->pest('--bail');
$delta = $project->delta();
expect($result->exitCode)->toBe(1, $result->describe())
->and($result->tally())->toContain('1 failed')
->and($delta->removed())->toBe(0, $delta->summary())
->and($delta->structureMoved())->toBeFalse($delta->summary());
})->skipOnWindows();
test('a green bail run is complete', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--bail');
$delta = $project->delta();
expect($result->exitCode)->toBe(0, $result->describe())
->and($delta->writtenCount())->toBe(Project::TOTAL_TESTS, $delta->summary())
->and($delta->removed())->toBe(0, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('--no-tia refreshes results without enabling tia', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--no-tia');
$delta = $project->delta();
expect($result->output)->not->toContain('Experimental TIA mode enabled')
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
->and($delta->writtenCount())->toBe(Project::TOTAL_TESTS, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('a run that never enables tia creates no graph', function (array $arguments): void {
$project = Project::make('master');
$result = $project->pest(...$arguments);
expect($result->exitCode)->toBe(0, $result->describe())
->and($project->graphExists())->toBeFalse();
})->with([
'plain' => [[]],
'filtered' => [['--filter=adds two numbers']],
])->skipOnWindows();
test('a test edit narrows to the affected file and replays the rest', function (): void {
$project = Project::make('master');
$project->seed('master');
$project->write('tests/Unit/GreeterTest.php', <<<'PHP'
<?php
declare(strict_types=1);
use Fixture\App\Greeter;
test('greets a person', function (): void {
expect((new Greeter)->greet('Nuno'))->toBe('Hello, Nuno!');
expect((new Greeter)->greet('Nuno'))->toBeString();
});
test('greets the world', function (): void {
expect((new Greeter)->greet('world'))->toBe('Hello, world!');
});
PHP);
$result = $project->pest('--tia');
$delta = $project->delta();
expect($result->affected())->toBe(2, $result->describe())
->and($result->replayed())->toBe(4, $result->describe())
->and($delta->writtenCount())->toBe(2, $delta->summary())
->and($delta->edgesMoved())->toBeFalse($delta->summary())
->and($delta->removed())->toBe(0, $delta->summary());
})->skipOnWindows();
test('a parallel run merges worker results into the parent baseline', function (): void {
$project = Project::make('master');
$project->seed('master');
$project->write('tests/Unit/GreeterTest.php', <<<'PHP'
<?php
declare(strict_types=1);
use Fixture\App\Greeter;
test('greets a person', function (): void {
expect((new Greeter)->greet('Nuno'))->toBe('Hello, Nuno!');
expect((new Greeter)->greet('Nuno'))->toBeString();
});
test('greets the world', function (): void {
expect((new Greeter)->greet('world'))->toBe('Hello, world!');
});
PHP);
$result = $project->pest('--tia', '--parallel', '--processes=2');
$delta = $project->delta();
expect($result->affected())->toBe(2, $result->describe())
->and($result->replayed())->toBe(4, $result->describe())
->and($delta->writtenCount())->toBe(2, $delta->summary())
->and($delta->edgesMoved())->toBeFalse($delta->summary())
->and($delta->removed())->toBe(0, $delta->summary());
})->skipOnWindows();
@@ -54,6 +54,7 @@ test('the CI provider names the default branch where the checkout cannot', funct
$project = Project::make('master');
$project->git()->unsetOriginHead();
$project->seed('master');
$project->addBaseline('legacy');
$project->git()->switchTo('feature-x', new: true);
@@ -73,6 +74,7 @@ test('GitLab names the default branch through its own variable', function (): vo
$project = Project::make('master');
$project->git()->unsetOriginHead();
$project->seed('master');
$project->addBaseline('legacy');
$project->git()->switchTo('feature-x', new: true);
@@ -124,7 +126,7 @@ test('an init.defaultBranch naming a branch that exists is still trusted', funct
expect($result->exitCode)->toBe(0, $result->describe())
->and($result->output)->not->toContain('could not determine the default branch')
->and($project->branchKeys())->toBe(['feature-x']);
->and($project->branchKeys())->not->toContain('master');
})->skipOnWindows();
test('a repository with no remote is refused rather than silently re-run', function (): void {
+16 -21
View File
@@ -14,37 +14,32 @@ test('narrows to the affected tests on a new branch', function (): void {
$project->git()->switchTo('feature-x', new: true);
$project->write('app/Calculator.php', <<<'PHP'
$project->write('tests/Unit/GreeterTest.php', <<<'PHP'
<?php
declare(strict_types=1);
namespace Fixture\App;
use Fixture\App\Greeter;
final class Calculator
{
public function add(int $a, int $b): int
{
return $a + $b;
}
test('greets a person', function (): void {
expect((new Greeter)->greet('Nuno'))->toBe('Hello, Nuno!');
expect((new Greeter)->greet('Nuno'))->toBeString();
});
public function subtract(int $a, int $b): int
{
return $a - $b;
}
public function multiply(int $a, int $b): int
{
return $a * $b;
}
}
test('greets the world', function (): void {
expect((new Greeter)->greet('world'))->toBe('Hello, world!');
});
PHP);
$result = $project->pest('--tia');
$delta = $project->delta();
expect($result->affected())->toBe(4, $result->describe())
->and($result->replayed())->toBe(2, $result->describe())
->and($result->exitCode)->toBe(0, $result->describe());
expect($result->affected())->toBe(2, $result->describe())
->and($result->replayed())->toBe(4, $result->describe())
->and($result->exitCode)->toBe(0, $result->describe())
->and($project->branchKeys())->toBe(['master', 'feature-x'])
->and($delta->baselineUntouched('master'))->toBeTrue($delta->summary())
->and($delta->edgesMoved())->toBeFalse($delta->summary());
})->skipOnWindows();
test('filtered mode reads the fallback too', function (): void {
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
use Tests\Fixtures\Tia\Project;
afterEach(function (): void {
Project::destroyAll();
});
test('re-runs a cached failure on a clean tree', function (): void {
$project = Project::make('master');
$project->seed('master', failing: ['adds two numbers']);
$result = $project->pest('--tia', '--filtered');
$delta = $project->delta();
expect($result->output)->toContain('from 1 previously unsuccessful test')
->and($result->affected())->toBe(2, $result->describe())
->and($result->tally())->toContain('2 passed')
->and($delta->writtenCount())->toBe(2, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('an explicit path turns filtered mode off', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--filtered', 'tests/Unit');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($result->output)->not->toContain('No affected tests found')
->and($result->tally())->toContain('4 passed')
->and($delta->writtenCount())->toBe(4, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('filtered mode records a baseline when there is none', function (): void {
$project = Project::make('master');
$result = $project->pest('--tia', '--filtered');
expect($result->exitCode)->toBe(0, $result->describe())
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
->and($result->output)->not->toContain('No affected tests found');
})->skipOnWindows();
test('filtered mode finds nothing to do in parallel either', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--filtered', '--parallel', '--processes=2');
$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 corrupt graph is rebuilt rather than crashing the run', function (): void {
$project = Project::make('master');
$project->seed('master');
file_put_contents($project->graphDir().'/graph.json', '{not json');
$result = $project->pest('--tia');
expect($result->exitCode)->toBe(0, $result->describe())
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed');
})->skipOnWindows();
test('--parallel --retry is refused and leaves the graph alone', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--parallel', '--retry');
$delta = $project->delta();
expect($result->exitCode)->not->toBe(0)
->and($result->output)->toContain('--retry')
->and($delta->isHardSuppressed())->toBeTrue($delta->summary());
})->skipOnWindows();
+169
View File
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
use Tests\Fixtures\Tia\Project;
afterEach(function (): void {
Project::destroyAll();
});
test('a filtered run rewrites only the test that ran', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--filter=adds two numbers');
$delta = $project->delta();
expect($result->tally())->toContain('1 passed')
->and($result->output)->not->toContain('TIA does not apply to partial runs')
->and($delta->writtenCount())->toBe(1, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('a filtered run under --tia announces that tia does not apply', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--filter=adds two numbers');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($delta->writtenCount())->toBe(1, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('a test suffix narrows the tier even though every test runs', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--test-suffix=Test.php');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
->and($delta->writtenCount())->toBe(Project::TOTAL_TESTS, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('a dirty run narrows to the uncommitted test edit', function (): void {
$project = Project::make('master');
$project->seed('master');
$project->write('tests/Unit/GreeterTest.php', <<<'PHP'
<?php
declare(strict_types=1);
use Fixture\App\Greeter;
test('greets a person', function (): void {
expect((new Greeter)->greet('Nuno'))->toBe('Hello, Nuno!');
});
test('greets the world', function (): void {
expect((new Greeter)->greet('world'))->toBe('Hello, world!');
});
test('greets again', function (): void {
expect((new Greeter)->greet('again'))->toBe('Hello, again!');
});
PHP);
$result = $project->pest('--tia', '--dirty');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($result->tally())->toContain('3 passed')
->and($delta->removed())->toBe(0, $delta->summary())
->and($delta->structureMoved())->toBeFalse($delta->summary());
})->skipOnWindows();
test('filtered mode yields to an explicit filter', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--filtered', '--filter=adds two numbers');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($result->tally())->toContain('1 passed')
->and($delta->writtenCount())->toBe(1, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('an env flag narrows exactly like the option it mirrors', function (string $variable): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pestWithEnvironment($project->path(), [
$variable => '1',
], '--filter=adds two numbers');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($delta->writtenCount())->toBe(1, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->with(['PEST_TIA', 'PEST_TIA_FILTERED'])->skipOnWindows();
test('a partial run does not purge the graph even with --fresh', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--fresh', '--filter=adds two numbers');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($delta->writtenCount())->toBe(1, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('--no-tia does not stop a partial run from refreshing its own entry', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--no-tia', '--filter=adds two numbers');
$delta = $project->delta();
expect($result->output)->not->toContain('TIA does not apply to partial runs')
->and($delta->writtenCount())->toBe(1, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('two partial runs each keep the other entry', function (): void {
$project = Project::make('master');
$project->seed('master');
$project->pest('--filter=adds two numbers');
$project->pest('--filter=greets a person');
$delta = $project->delta();
expect($delta->writtenCount())->toBe(2, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('a shard is a partial run', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--shard=1/2');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($delta->removed())->toBe(0, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
test('a parallel partial run writes nothing at all', function (): void {
$project = Project::make('master');
$project->seed('master');
$result = $project->pest('--tia', '--parallel', '--processes=2', '--filter=adds two numbers');
$delta = $project->delta();
expect($result->output)->toContain('TIA does not apply to partial runs')
->and($delta->writtenCount())->toBe(0, $delta->summary())
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
})->skipOnWindows();
+13
View File
@@ -259,6 +259,19 @@ final class Project
});
}
/**
* Adds a second, empty baseline key, so a lone recorded baseline can no
* longer stand in for the default branch.
*/
public function addBaseline(string $branch): void
{
$this->mutateGraph(function (array $graph) use ($branch): array {
$graph['baselines'][$branch] = ['sha' => null, 'tree' => [], 'results' => []];
return $graph;
});
}
/**
* @param callable(array<string, mixed>): array<string, mixed> $callback
*/