mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 06:13:35 +02:00
wq
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Fixtures\Tia\Project;
|
||||
|
||||
afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
/*
|
||||
* Invariant 5 — writes land on the branch that ran and only there — and
|
||||
* invariant 6 — nothing is unbounded — under every branch shape git allows.
|
||||
*/
|
||||
|
||||
test('a branch name git allows is a branch key TIA can hold', function (string $branch): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo($branch, new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($result->uncached())->toBe(0, $result->describe())
|
||||
->and($project->branchKeys())->toBe(['master', $branch])
|
||||
->and($delta->baselineUntouched('master'))->toBeTrue($delta->summary());
|
||||
})->with([
|
||||
'slashes' => 'feature/deep/nesting',
|
||||
'dots' => 'release.1.2.x',
|
||||
'unicode' => 'feature-café-日本',
|
||||
'digits' => '12345',
|
||||
'underscores' => 'feature_x_y',
|
||||
'very long' => 'feature-'.str_repeat('x', 180),
|
||||
])->skipOnWindows();
|
||||
|
||||
test('a branch differing from the default only in case gets its own key', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('MASTER-2', new: true);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($project->branchKeys())->toBe(['master', 'MASTER-2']);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a branch that only lives on the remote keeps its baseline', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('remote-only', new: true);
|
||||
$project->pest('--tia');
|
||||
|
||||
$project->git()->switchTo('master');
|
||||
$project->git()->run(['update-ref', 'refs/remotes/origin/remote-only', 'HEAD']);
|
||||
$project->git()->run(['branch', '-D', 'remote-only']);
|
||||
|
||||
$project->pest('--tia');
|
||||
|
||||
expect($project->branchKeys())->toBe(['master', 'remote-only']);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a branch checked out in a worktree keeps its baseline', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('wt-branch', new: true);
|
||||
$project->pest('--tia');
|
||||
|
||||
$project->git()->switchTo('master');
|
||||
$project->git()->run(['worktree', 'add', '--quiet', $project->path().'-wt', 'wt-branch']);
|
||||
|
||||
$project->pest('--tia');
|
||||
|
||||
$project->git()->run(['worktree', 'remove', '--force', $project->path().'-wt']);
|
||||
|
||||
expect($project->branchKeys())->toBe(['master', 'wt-branch']);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('deleting many branches reclaims every one of their baselines', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
foreach (range(1, 4) as $index) {
|
||||
$project->git()->switchTo('feature-'.$index, new: true);
|
||||
$project->pest('--tia', ...$arguments);
|
||||
}
|
||||
|
||||
expect($project->branchKeys())->toHaveCount(5);
|
||||
|
||||
$project->git()->switchTo('master');
|
||||
|
||||
foreach (range(1, 4) as $index) {
|
||||
$project->git()->run(['branch', '-D', 'feature-'.$index]);
|
||||
}
|
||||
|
||||
$project->pest('--tia', ...$arguments);
|
||||
|
||||
expect($project->branchKeys())->toBe(['master']);
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a narrowed run does not reclaim anything', 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()->run(['branch', '-D', 'feature-x']);
|
||||
|
||||
$project->snapshot();
|
||||
$project->pest('--tia', '--filter=adds two numbers');
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($project->branchKeys())->toBe(['master', 'feature-x'])
|
||||
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a detached HEAD does not reclaim anything either', 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()->run(['branch', '-D', 'feature-x']);
|
||||
$project->git()->detach();
|
||||
|
||||
$project->snapshot();
|
||||
$project->pest('--tia');
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($project->branchKeys())->toBe(['master', 'feature-x'])
|
||||
->and($delta->isHardSuppressed())->toBeTrue($delta->summary());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('the default branch baseline survives every branch that comes and goes', 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()->run(['branch', '-D', 'feature-x']);
|
||||
|
||||
$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->added())->toBe(0, $delta->summary())
|
||||
->and($delta->removed())->toBe(0, $delta->summary())
|
||||
->and($project->graph()['baselines']['master']['results'])->toHaveCount(Project::TOTAL_TESTS);
|
||||
})->skipOnWindows();
|
||||
@@ -75,10 +75,13 @@ test('filtered mode falls back to a full replay when a cached failure cannot be
|
||||
|
||||
$project->seed('master', failing: ['adds two numbers']);
|
||||
|
||||
// A path this project cannot address at all — recorded on another machine.
|
||||
// A path that merely no longer exists is a *deleted* test, not a lost one,
|
||||
// and widening the run would not find it either; see StateReclamation.
|
||||
$project->mutateGraph(function (array $graph): array {
|
||||
$testId = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
|
||||
$graph['baselines']['master']['results'][$testId]['file'] = 'tests/Unit/DeletedTest.php';
|
||||
$graph['baselines']['master']['results'][$testId]['file'] = '/build/agent/tests/Unit/DeletedTest.php';
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Fixtures\Tia\Project;
|
||||
|
||||
afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
/*
|
||||
* Invariant 7 — a hostile state dir cannot break a run. Whatever is in
|
||||
* graph.json, the suite still runs and exits on the tests' merit.
|
||||
*/
|
||||
|
||||
test('a graph mangled beyond use still lets the suite run', function (string $contents): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
file_put_contents($project->graphDir().'/graph.json', $contents);
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed');
|
||||
})->with([
|
||||
'empty' => '',
|
||||
'truncated' => '{"schema":1,"files":["app/Calculator.php"],"edg',
|
||||
'not json' => '{not json',
|
||||
'json scalar' => '"just a string"',
|
||||
'json list' => '[1,2,3]',
|
||||
'json null' => 'null',
|
||||
'empty object' => '{}',
|
||||
'nul bytes' => "\0\0\0\0",
|
||||
])->skipOnWindows();
|
||||
|
||||
test('a graph whose shape is wrong everywhere is repaired rather than trusted', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
|
||||
$graph['baselines']['master']['results'][$id] = 'nope';
|
||||
$graph['baselines']['master']['results'][7] = ['status' => 0, 'message' => '', 'time' => 0.1];
|
||||
$graph['baselines']['master']['tree'] = 'nope';
|
||||
$graph['baselines']['master']['sha'] = 42;
|
||||
$graph['baselines'][''] = ['sha' => null, 'tree' => [], 'results' => []];
|
||||
$graph['baselines']['broken'] = 'nope';
|
||||
$graph['edges']['tests/Unit/GreeterTest.php'] = 'nope';
|
||||
$graph['edges'][''] = [0];
|
||||
$graph['files'][] = ['nested'];
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia', ...$arguments);
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->output)->not->toContain('TypeError')
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
|
||||
->and($project->branchKeys())->toBe(['master']);
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a cached status this build cannot interpret is re-run, not replayed', function (int $status): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph) use ($status): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
$graph['baselines']['master']['results'][$id]['status'] = $status;
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
|
||||
->and($result->uncached())->toBe(1, $result->describe());
|
||||
})->with([
|
||||
'below the range' => -1,
|
||||
'one past the range' => 9,
|
||||
'far past the range' => 99,
|
||||
'huge' => PHP_INT_MAX,
|
||||
])->skipOnWindows();
|
||||
|
||||
test('a cached status with no replay of its own does not fail the run', function (int $status): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph) use ($status): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
$graph['baselines']['master']['results'][$id]['status'] = $status;
|
||||
$graph['baselines']['master']['results'][$id]['message'] = 'cached detail';
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed');
|
||||
})->with(['notice' => 3, 'deprecation' => 4, 'warning' => 6])->skipOnWindows();
|
||||
|
||||
test('a cached skip or todo replays with its message intact', function (int $status, string $tally): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph) use ($status): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
$graph['baselines']['master']['results'][$id]['status'] = $status;
|
||||
$graph['baselines']['master']['results'][$id]['message'] = 'a recorded reason';
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain($tally)
|
||||
->and($result->output)->toContain('a recorded reason')
|
||||
->and($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe());
|
||||
})->with([
|
||||
'skipped' => [1, '1 skipped'],
|
||||
'incomplete' => [2, '1 incomplete'],
|
||||
])->skipOnWindows();
|
||||
|
||||
test('a cached failure with a multi-line message re-runs rather than replaying the text', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
$graph['baselines']['master']['results'][$id]['status'] = 7;
|
||||
$graph['baselines']['master']['results'][$id]['message'] = "line one\nline two\nline three";
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->uncached())->toBe(1, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed');
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a result pointing outside the project is not addressable and widens the run', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master', failing: ['adds two numbers']);
|
||||
|
||||
$project->mutateGraph(function (array $graph): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
$graph['baselines']['master']['results'][$id]['file'] = '/build/agent/tests/Unit/CalculatorTest.php';
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia', '--filtered');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->output)->toContain('could not be located on disk')
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed');
|
||||
})->skipOnWindows();
|
||||
|
||||
test('an edge pointing at a file id that does not exist is ignored', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph): array {
|
||||
$graph['edges']['tests/Unit/CalculatorTest.php'] = [0, 999, -5];
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a graph from a schema this build does not know is rebuilt, not read', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph): array {
|
||||
$graph['schema'] = 2;
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
|
||||
->and($result->replayed())->toBe(0, $result->describe());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('graph.json being a directory does not stop the run', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
unlink($project->graphDir().'/graph.json');
|
||||
mkdir($project->graphDir().'/graph.json');
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed');
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a state dir it cannot write to still replays', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
chmod($project->graphDir(), 0500);
|
||||
|
||||
try {
|
||||
$result = $project->pest('--tia');
|
||||
} finally {
|
||||
chmod($project->graphDir(), 0700);
|
||||
}
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe());
|
||||
})->skipOnWindows();
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Fixtures\Tia\Project;
|
||||
|
||||
afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
/*
|
||||
* A test that triggers a notice, deprecation or warning is reported by PHPUnit
|
||||
* as passed, and emits Passed. Recording it as a plain success made the cache
|
||||
* hide the issue: a later run under --fail-on-* came back green where a fresh
|
||||
* run failed. Invariant 3 — replay is faithful — at its most dangerous.
|
||||
*/
|
||||
|
||||
function tiaTriggering(string $call): string
|
||||
{
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Fixture\App\Calculator;
|
||||
|
||||
test('adds two numbers', function (): void {
|
||||
{$call}
|
||||
expect((new Calculator)->add(1, 2))->toBe(3);
|
||||
});
|
||||
|
||||
test('subtracts two numbers', function (): void {
|
||||
expect((new Calculator)->subtract(3, 1))->toBe(2);
|
||||
});
|
||||
PHP;
|
||||
}
|
||||
|
||||
test('a triggered issue is recorded as itself, not as a pass', function (array $arguments, string $call, int $status): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', tiaTriggering($call));
|
||||
$project->git()->commit('trigger an issue');
|
||||
|
||||
$project->pest('--tia', ...$arguments);
|
||||
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
|
||||
expect($project->graph()['baselines']['master']['results'][$id]['status'])->toBe($status);
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->with([
|
||||
'deprecation' => ["trigger_error('legacy adder', E_USER_DEPRECATED);", 4],
|
||||
'notice' => ["trigger_error('a notice', E_USER_NOTICE);", 3],
|
||||
'warning' => ["trigger_error('a warning', E_USER_WARNING);", 6],
|
||||
])->skipOnWindows();
|
||||
|
||||
test('a cached deprecation still fails the run that asked to fail on one', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', tiaTriggering("trigger_error('legacy adder', E_USER_DEPRECATED);"));
|
||||
$project->git()->commit('trigger a deprecation');
|
||||
|
||||
$project->pest('--tia', ...$arguments);
|
||||
|
||||
$result = $project->pest('--tia', '--fail-on-deprecation', ...$arguments);
|
||||
|
||||
expect($result->exitCode)->toBe(1, $result->describe())
|
||||
->and($result->tally())->toContain('1 deprecated')
|
||||
->and($result->uncached())->toBe(1, $result->describe());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('replaying a cached issue does not downgrade it to a pass', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', tiaTriggering("trigger_error('legacy adder', E_USER_DEPRECATED);"));
|
||||
$project->git()->commit('trigger a deprecation');
|
||||
|
||||
$project->pest('--tia', ...$arguments);
|
||||
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
|
||||
foreach (range(1, 3) as $ignored) {
|
||||
$project->pest('--tia', ...$arguments);
|
||||
|
||||
expect($project->graph()['baselines']['master']['results'][$id]['status'])->toBe(4);
|
||||
}
|
||||
|
||||
$result = $project->pest('--tia', '--fail-on-deprecation', ...$arguments);
|
||||
|
||||
expect($result->exitCode)->toBe(1, $result->describe());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a failure outranks an issue triggered on the way to it', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', str_replace(
|
||||
'toBe(3)',
|
||||
'toBe(999)',
|
||||
tiaTriggering("trigger_error('legacy adder', E_USER_DEPRECATED);"),
|
||||
));
|
||||
$project->git()->commit('an issue then a failure');
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
|
||||
expect($result->exitCode)->not->toBe(0)
|
||||
->and($project->graph()['baselines']['master']['results'][$id]['status'])->toBe(7);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a skip outranks an issue triggered on the way to it', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', str_replace(
|
||||
'expect((new Calculator)->add(1, 2))->toBe(3);',
|
||||
"\$this->markTestSkipped('not today');",
|
||||
tiaTriggering("trigger_error('legacy adder', E_USER_DEPRECATED);"),
|
||||
));
|
||||
$project->git()->commit('an issue then a skip');
|
||||
|
||||
$project->pest('--tia');
|
||||
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
|
||||
expect($project->graph()['baselines']['master']['results'][$id]['status'])->toBe(1);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a suppressed issue is not recorded', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', tiaTriggering("@trigger_error('quiet', E_USER_DEPRECATED);"));
|
||||
$project->git()->commit('a suppressed deprecation');
|
||||
|
||||
$project->pest('--tia');
|
||||
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
|
||||
expect($project->graph()['baselines']['master']['results'][$id]['status'])->toBe(0);
|
||||
})->skipOnWindows();
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Fixtures\Tia\Project;
|
||||
|
||||
afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
test('a detached HEAD does not purge the graph on structural drift', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->detach();
|
||||
|
||||
$project->write('composer.lock', (string) json_encode([
|
||||
'content-hash' => 'drifted',
|
||||
'packages' => [],
|
||||
'packages-dev' => [],
|
||||
]));
|
||||
|
||||
$result = $project->pest('--tia', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($project->graphExists())->toBeTrue('the detached run deleted graph.json')
|
||||
->and($delta->isHardSuppressed())->toBeTrue($delta->summary());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a detached HEAD does not purge the graph with --fresh either', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->detach();
|
||||
|
||||
$result = $project->pest('--tia', '--fresh', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($project->graphExists())->toBeTrue('the detached --fresh run deleted graph.json')
|
||||
->and($delta->isHardSuppressed())->toBeTrue($delta->summary());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a detached HEAD leaves an unreadable graph for a checkout that can rebuild it', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->detach();
|
||||
|
||||
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')
|
||||
->and(file_get_contents($project->graphDir().'/graph.json'))->toBe('{not json');
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a cached failure whose test file was deleted stops widening later runs', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master', failing: ['adds two numbers']);
|
||||
|
||||
unlink($project->path('tests/Unit/CalculatorTest.php'));
|
||||
$project->git()->commit('drop CalculatorTest');
|
||||
|
||||
$project->pest('--tia', '--filtered', ...$arguments);
|
||||
|
||||
$project->snapshot();
|
||||
$second = $project->pest('--tia', '--filtered', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($second->output)->not->toContain('could not be located on disk')
|
||||
->and($second->output)->toContain('No affected tests found')
|
||||
->and($delta->isHardSuppressed())->toBeTrue($delta->summary());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a complete run reclaims the entry and the edge of a deleted test file', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master', failing: ['adds two numbers']);
|
||||
|
||||
unlink($project->path('tests/Unit/CalculatorTest.php'));
|
||||
$project->git()->commit('drop CalculatorTest');
|
||||
|
||||
$project->pest('--tia', ...$arguments);
|
||||
|
||||
$graph = $project->graph();
|
||||
|
||||
expect($graph['edges'] ?? [])->not->toHaveKey('tests/Unit/CalculatorTest.php')
|
||||
->and(array_column($graph['baselines']['master']['results'] ?? [], 'file'))
|
||||
->not->toContain('tests/Unit/CalculatorTest.php');
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a pruned result does not come back from the fallback', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master', failing: ['adds two numbers']);
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', <<<'PHP'
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Fixture\App\Calculator;
|
||||
|
||||
test('adds two numbers, renamed', function (): void {
|
||||
expect((new Calculator)->add(1, 2))->toBe(3);
|
||||
});
|
||||
|
||||
test('subtracts two numbers', function (): void {
|
||||
expect((new Calculator)->subtract(3, 1))->toBe(2);
|
||||
});
|
||||
PHP);
|
||||
|
||||
$project->git()->commit('rename the test on the branch');
|
||||
|
||||
$project->pest('--tia', '--filtered', ...$arguments);
|
||||
|
||||
$second = $project->pest('--tia', '--filtered', ...$arguments);
|
||||
|
||||
expect($second->output)->not->toContain('previously unsuccessful')
|
||||
->and($second->output)->toContain('No affected tests found');
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('the fallback still reaches a branch that has never run a test file', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master', failing: ['adds two numbers']);
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
|
||||
// A narrowed run mints the branch key holding only the Greeter entries, so
|
||||
// the layering must still serve master's cached failure for the Calculator.
|
||||
$project->pest('--filter=greets a person');
|
||||
|
||||
$result = $project->pest('--tia', '--filtered');
|
||||
|
||||
expect($result->output)->toContain('previously unsuccessful')
|
||||
->and($result->affected())->toBe(2, $result->describe());
|
||||
})->skipOnWindows();
|
||||
|
||||
test('a branch that git no longer knows loses its baseline', function (): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
$project->pest('--tia');
|
||||
|
||||
expect($project->branchKeys())->toBe(['master', 'feature-x']);
|
||||
|
||||
$project->git()->switchTo('master');
|
||||
$project->git()->run(['branch', '-D', 'feature-x']);
|
||||
|
||||
$project->pest('--tia');
|
||||
|
||||
expect($project->branchKeys())->toBe(['master']);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('an unknown cached status is re-run rather than replayed as a failure', function (int $status): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph) use ($status): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
$graph['baselines']['master']['results'][$id]['status'] = $status;
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
|
||||
->and($result->uncached())->toBe(1, $result->describe());
|
||||
})->with(['unknown' => -1, 'future' => 9, 'garbage' => 99])->skipOnWindows();
|
||||
|
||||
test('a cached notice, deprecation or warning does not replay as a failure', function (int $status): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph) use ($status): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
$graph['baselines']['master']['results'][$id]['status'] = $status;
|
||||
$graph['baselines']['master']['results'][$id]['message'] = 'cached detail';
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia');
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed');
|
||||
})->with(['notice' => 3, 'deprecation' => 4, 'warning' => 6])->skipOnWindows();
|
||||
|
||||
test('a malformed baseline entry cannot break the run', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->mutateGraph(function (array $graph): array {
|
||||
$id = Project::testId('tests/Unit/CalculatorTest.php', 'adds two numbers');
|
||||
$graph['baselines']['master']['results'][$id] = 'nope';
|
||||
$graph['baselines']['master']['tree'] = 'nope';
|
||||
$graph['edges']['tests/Unit/GreeterTest.php'] = 'nope';
|
||||
|
||||
return $graph;
|
||||
});
|
||||
|
||||
$result = $project->pest('--tia', ...$arguments);
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->output)->not->toContain('TypeError')
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed');
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
/*
|
||||
* Invariant 1 — sequential and parallel must agree — under a process that is
|
||||
* torn down in the middle of a test file. A worker that flushed what it got to
|
||||
* before dying has not seen enough of that file to license pruning the
|
||||
* siblings it never reached.
|
||||
*/
|
||||
test('a run torn down mid-file does not prune the tests it never reached', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', <<<'PHP'
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Fixture\App\Calculator;
|
||||
|
||||
test('adds two numbers', function (): void {
|
||||
expect((new Calculator)->add(1, 2))->toBe(3);
|
||||
});
|
||||
|
||||
test('subtracts two numbers', function (): void {
|
||||
exit(0);
|
||||
});
|
||||
PHP);
|
||||
|
||||
$project->git()->commit('a test that kills its own process');
|
||||
|
||||
$project->pest('--tia', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($delta->removed())->toBe(0, $delta->summary())
|
||||
->and($delta->shaMoved())->toBeFalse($delta->summary())
|
||||
->and($delta->structureMoved())->toBeFalse($delta->summary())
|
||||
->and(array_keys($project->graph()['baselines']['master']['results']))
|
||||
->toContain(Project::testId('tests/Unit/CalculatorTest.php', 'subtracts two numbers'));
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a fatal error mid-file is a test error, not a truncation', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->write('tests/Unit/CalculatorTest.php', <<<'PHP'
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Fixture\App\Calculator;
|
||||
|
||||
test('adds two numbers', function (): void {
|
||||
expect((new Calculator)->add(1, 2))->toBe(3);
|
||||
});
|
||||
|
||||
test('subtracts two numbers', function (): void {
|
||||
undefined_function_here();
|
||||
});
|
||||
PHP);
|
||||
|
||||
$project->git()->commit('a test that fatals');
|
||||
|
||||
$result = $project->pest('--tia', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->exitCode)->not->toBe(0)
|
||||
->and($result->tally())->toContain('1 failed')
|
||||
->and($delta->removed())->toBe(0, $delta->summary())
|
||||
->and($delta->structureMoved())->toBeFalse($delta->summary());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
/*
|
||||
* Invariant 2 — every command lands in exactly one tier and stays inside it —
|
||||
* for the combinations that were never exercised.
|
||||
*/
|
||||
test('a green complete run leaves the graph exactly as it found it', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$result = $project->pest('--tia', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($delta->isHardSuppressed())->toBeTrue($delta->summary());
|
||||
})->with([
|
||||
'bail' => [['--bail']],
|
||||
'stop-on-failure' => [['--stop-on-failure']],
|
||||
'compact' => [['--compact']],
|
||||
'parallel bail' => [['--parallel', '--processes=2', '--bail']],
|
||||
'parallel one process' => [['--parallel', '--processes=1']],
|
||||
'parallel more processes than files' => [['--parallel', '--processes=8']],
|
||||
])->skipOnWindows();
|
||||
|
||||
test('--tia --no-tia is a plain run that still refreshes what it executed', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$result = $project->pest('--tia', '--no-tia', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->replayed())->toBe(0, $result->describe())
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
|
||||
->and($delta->writtenCount())->toBe(Project::TOTAL_TESTS, $delta->summary())
|
||||
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('--fresh on a partial run neither purges nor prunes', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$result = $project->pest('--tia', '--fresh', '--filter=adds two numbers', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($project->graphExists())->toBeTrue()
|
||||
->and($delta->writtenCount())->toBe(1, $delta->summary())
|
||||
->and($delta->isResultsOnly())->toBeTrue($delta->summary());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
|
||||
test('a second green run on a feature branch writes nothing at all', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
$project->seed('master');
|
||||
|
||||
$project->git()->switchTo('feature-x', new: true);
|
||||
$project->pest('--tia', ...$arguments);
|
||||
|
||||
$project->snapshot();
|
||||
$result = $project->pest('--tia', ...$arguments);
|
||||
$delta = $project->delta();
|
||||
|
||||
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||
->and($delta->isHardSuppressed())->toBeTrue($delta->summary());
|
||||
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
|
||||
@@ -87,7 +87,7 @@ final readonly class GraphDelta
|
||||
*/
|
||||
public function branchKeys(): array
|
||||
{
|
||||
return array_keys($this->baselines($this->after));
|
||||
return array_map(strval(...), array_keys($this->baselines($this->after)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +95,7 @@ final readonly class GraphDelta
|
||||
*/
|
||||
public function branchKeysBefore(): array
|
||||
{
|
||||
return array_keys($this->baselines($this->before));
|
||||
return array_map(strval(...), array_keys($this->baselines($this->before)));
|
||||
}
|
||||
|
||||
public function branchKeysMoved(): bool
|
||||
@@ -254,9 +254,7 @@ final readonly class GraphDelta
|
||||
$baselines = [];
|
||||
|
||||
foreach ($this->section($graph, 'baselines') as $branch => $baseline) {
|
||||
if (is_string($branch)) {
|
||||
$baselines[$branch] = $baseline;
|
||||
}
|
||||
$baselines[(string) $branch] = $baseline;
|
||||
}
|
||||
|
||||
return $baselines;
|
||||
|
||||
@@ -322,7 +322,8 @@ final class Project
|
||||
{
|
||||
$baselines = $this->graph()['baselines'] ?? [];
|
||||
|
||||
return is_array($baselines) ? array_keys($baselines) : [];
|
||||
// A branch named `12345` comes back from json_decode as an integer key.
|
||||
return is_array($baselines) ? array_map(strval(...), array_keys($baselines)) : [];
|
||||
}
|
||||
|
||||
public function graphDir(): string
|
||||
|
||||
Reference in New Issue
Block a user