mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 06:13:35 +02:00
wip
This commit is contained in:
+4
-85
@@ -124,18 +124,14 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
'--compact', '--ci-build-id', '--min',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
/** @var list<string> */
|
||||
private const array COVERAGE_REPORT_FLAGS = [
|
||||
'--coverage-clover', '--coverage-cobertura', '--coverage-crap4j',
|
||||
'--coverage-html', '--coverage-openclover', '--coverage-php',
|
||||
'--coverage-text', '--coverage-xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
/** @var list<string> */
|
||||
private const array PARTIAL_SELECTION_FLAGS = [
|
||||
'--filter', '--exclude-filter', '--group', '--exclude-group',
|
||||
'--covers', '--uses', '--testsuite', '--exclude-testsuite', '--test-suffix',
|
||||
@@ -160,19 +156,10 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
/** @var array<string, int> */
|
||||
private array $cachedAssertionsByTestId = [];
|
||||
|
||||
/**
|
||||
* The status a replayed test was replayed *as*, so the write-back records
|
||||
* what was cached rather than what the replay looked like from the
|
||||
* outside. A cached deprecation replays as a pass — recording that pass
|
||||
* would erase the deprecation from the baseline on the very next run.
|
||||
*
|
||||
* @var array<string, array{status: int, message: string}>
|
||||
*/
|
||||
/** @var array<string, array{status: int, message: string}> */
|
||||
private array $cachedStatusByTestId = [];
|
||||
|
||||
/**
|
||||
* @var array<string, float>
|
||||
*/
|
||||
/** @var array<string, float> */
|
||||
private array $cachedTimeByTestId = [];
|
||||
|
||||
private ?Graph $replayGraph = null;
|
||||
@@ -272,11 +259,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
return $graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a graph that will not decode, so the next run that can record starts
|
||||
* clean instead of tripping over the same file forever — rebuilding needs a
|
||||
* coverage driver, and without one the file would stay corrupt for good.
|
||||
*/
|
||||
private function discardUnreadableGraph(): void
|
||||
{
|
||||
if (Parallel::isWorker()) {
|
||||
@@ -297,16 +279,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
$this->renderBadge('WARN', 'The dependency graph could not be read — it will be rebuilt.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a state file, unless this checkout may not write.
|
||||
*
|
||||
* A detached HEAD names no branch, so {@see self::saveGraph()} refuses to
|
||||
* write — which means anything deleted here could never be rebuilt from
|
||||
* this checkout. Read-only has to mean deletes too, or a drifted
|
||||
* `composer.lock` on a detached CI checkout wipes the whole team's baseline.
|
||||
*
|
||||
* @return bool Whether the delete happened.
|
||||
*/
|
||||
private function deleteState(string $key): bool
|
||||
{
|
||||
if ($this->detachedHead) {
|
||||
@@ -318,9 +290,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
|
||||
private function saveGraph(Graph $graph): bool
|
||||
{
|
||||
// A detached HEAD names no branch of its own, so `$this->branch` is the
|
||||
// fallback — writing here would land this checkout's results in the
|
||||
// default branch's baseline. Leave the graph exactly as it was.
|
||||
if ($this->detachedHead) {
|
||||
return true;
|
||||
}
|
||||
@@ -580,9 +549,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
$this->flushWorkerReplay();
|
||||
}
|
||||
|
||||
// `terminate()` also runs from the shutdown handler, which is how a run
|
||||
// that `exit()`s inside a test gets here — with a test prepared and
|
||||
// never finished, and so with no right to a complete write.
|
||||
if ($this->writesSuppressed || $this->resultsOnlyWrites || $this->hasUnfinishedTest()) {
|
||||
$this->recorder->reset();
|
||||
$this->coverageCollector->reset();
|
||||
@@ -695,16 +661,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
// Re-anchor the baseline. Reaching here means the run was complete —
|
||||
// nothing suppressed, narrowed or truncated it — so its results are the
|
||||
// truth at HEAD and the recorded revision may say so.
|
||||
//
|
||||
// That matters most when the recorded commit had become unreachable (a
|
||||
// rebase, a force-push) and no coverage driver was available to rebuild:
|
||||
// without this the stale revision survives, and every later run warns
|
||||
// and re-runs the whole suite, for good. Stale edges are no objection —
|
||||
// a complete run just re-recorded every result, and later changes are
|
||||
// compared against the revision written here.
|
||||
if ($this->replayRan || $this->graphUnreachable) {
|
||||
$this->bumpRecordedSha();
|
||||
}
|
||||
@@ -852,9 +808,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
try {
|
||||
$this->resolveBranch($projectRoot);
|
||||
} catch (MissingDependency $missingGit) {
|
||||
// Every git call TIA makes fails on `HEAD` in a repository that has
|
||||
// no commits yet, which reads as "git is missing" when git is right
|
||||
// there. Say what is actually wrong instead.
|
||||
$repository = new ChangedFiles($projectRoot);
|
||||
|
||||
if ($repository->isRepository() && ! $repository->hasCommits()) {
|
||||
@@ -913,21 +866,12 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
$this->state->write(self::KEY_COVERAGE_MARKER, '');
|
||||
}
|
||||
|
||||
// An active coverage report owns the driver, so edges have to be
|
||||
// piggybacked off its session — and that session is scoped to
|
||||
// phpunit.xml's <source>, not to the whole project. Refreshing an
|
||||
// existing graph that way is safe (`replaceEdges()` keeps what it
|
||||
// already has), but *founding* one on it is not: every source file
|
||||
// outside the coverage scope would be missing from the graph for good,
|
||||
// and a change to one of them would select nothing and replay a pass.
|
||||
if (! $graph instanceof Graph && $this->piggybackCoverage) {
|
||||
$this->emitCoverageScopedRecordSkipped();
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
// Past the guard above, a coverage-owned run always has a graph to
|
||||
// refresh — a run without one never gets here.
|
||||
if ($coverageCacheOwned && ! $this->state->exists(self::KEY_COVERAGE_CACHE)) {
|
||||
if ($this->driftLabel === null) {
|
||||
$this->freshGraphReason = 'recording a coverage baseline';
|
||||
@@ -1392,15 +1336,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
$this->renderChild('Install / enable pcov or xdebug (mode: coverage) in the worker PHP and rerun.');
|
||||
}
|
||||
|
||||
/**
|
||||
* A parallel run keeps its results in the workers, so the parent's collector
|
||||
* is empty and nothing would ever reach the graph. Ask the workers to flush
|
||||
* what they ran, so a parallel run refreshes — and prunes — exactly like the
|
||||
* sequential run of the same command.
|
||||
*
|
||||
* Gated on a graph already existing: a project that has never run TIA must
|
||||
* not gain a baseline from a plain `--parallel` run.
|
||||
*/
|
||||
private function requestWorkerResults(): void
|
||||
{
|
||||
if (Parallel::isWorker() || ! Parallel::isEnabled() || $this->writesSuppressed) {
|
||||
@@ -1772,16 +1707,8 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
$collector->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Give back what the graph no longer needs. Only ever called from a
|
||||
* complete write — the RESULTS-ONLY and HARD-SUPPRESSED tiers may not
|
||||
* remove an entry, and a narrowed run has not seen enough to judge.
|
||||
*/
|
||||
private function reclaim(Graph $graph): void
|
||||
{
|
||||
// The fallback branch never layers under itself, so marking it would
|
||||
// write the graph for no reader's benefit — and cost a clean green run
|
||||
// its "wrote nothing at all".
|
||||
if ($this->branch !== $this->fallbackBranch) {
|
||||
$graph->markBaselineComplete($this->branch);
|
||||
}
|
||||
@@ -1795,10 +1722,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
return;
|
||||
}
|
||||
|
||||
// A shallow, single-branch CI checkout can see almost no refs, and
|
||||
// "git has never heard of it" would then mean "this clone is narrow",
|
||||
// not "that branch is gone". Only reclaim from a checkout that can at
|
||||
// least see the branch everything else falls back to.
|
||||
if (! in_array($this->fallbackBranch, $branches, true)) {
|
||||
return;
|
||||
}
|
||||
@@ -1966,10 +1889,6 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
return TestResultFacade::shouldStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* A test that was prepared and never finished means this process is being
|
||||
* torn down mid-file, so it has not seen enough of that file to prune it.
|
||||
*/
|
||||
private function hasUnfinishedTest(): bool
|
||||
{
|
||||
$collector = Container::getInstance()->get(ResultCollector::class);
|
||||
|
||||
@@ -244,11 +244,7 @@ final readonly class ChangedFiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Every branch name this checkout knows, local and remote alike. Remotes
|
||||
* count: a branch that only lives on the origin is still a branch someone
|
||||
* will check out, and its baseline must survive.
|
||||
*
|
||||
* @return list<string>|null `null` when git cannot answer.
|
||||
* @return list<string>|null
|
||||
*/
|
||||
public function branchNames(): ?array
|
||||
{
|
||||
@@ -307,13 +303,6 @@ final readonly class ChangedFiles
|
||||
return $process->getExitCode() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this repository has a revision to anchor a baseline to.
|
||||
*
|
||||
* A freshly initialised repository has none, and every other git call TIA
|
||||
* makes — {@see self::currentBranch()}, {@see self::currentSha()} — fails on
|
||||
* `HEAD` there and reports git as missing, which it is not.
|
||||
*/
|
||||
public function hasCommits(): bool
|
||||
{
|
||||
$process = new Process(['git', 'rev-parse', '--verify', '--quiet', 'HEAD'], $this->projectRoot);
|
||||
@@ -357,10 +346,6 @@ final readonly class ChangedFiles
|
||||
*/
|
||||
private function diffSinceSha(string $sha): array
|
||||
{
|
||||
// `--no-renames` matters: with rename detection on, git reports only the
|
||||
// destination of a moved file, so the path the graph has edges for — the
|
||||
// one that is gone — never reaches selection, and every test that
|
||||
// depended on it replays its recorded pass.
|
||||
$process = new Process(
|
||||
['git', 'diff', '--name-only', '--no-renames', $sha.'..HEAD'],
|
||||
$this->projectRoot,
|
||||
|
||||
@@ -29,10 +29,6 @@ enum ReplayType
|
||||
$status->isRisky() => self::Risky,
|
||||
$status->isSkipped() => self::Skipped,
|
||||
$status->isIncomplete() => self::Incomplete,
|
||||
// A recorded notice, deprecation or warning only reaches replay when
|
||||
// the configured failOn* / displayDetailsOn* policies say it is not
|
||||
// worth re-running — which means the test passed. Folding it into
|
||||
// Failure below would turn a green run red on cache alone.
|
||||
$status->isNotice(), $status->isDeprecation(), $status->isWarning() => self::Pass,
|
||||
$status->isFailure(), $status->isError() => self::Failure,
|
||||
default => self::None,
|
||||
|
||||
@@ -120,14 +120,6 @@ final class Graph
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep only the test files this checkout actually has.
|
||||
*
|
||||
* A stale edge key — a test file a fetched baseline knew, or one another
|
||||
* branch deleted — cannot be run by anyone, and selecting it strands
|
||||
* `--filtered` on a run that matches nothing and reports success on a
|
||||
* change no test looked at. {@see self::testFilesToRerun()} has always
|
||||
* dropped these; the change-driven half of selection must agree.
|
||||
*
|
||||
* @param array<int, string> $testFiles Project-relative paths.
|
||||
* @return list<string>
|
||||
*/
|
||||
@@ -677,9 +669,6 @@ final class Graph
|
||||
|
||||
$r = $baseline['results'][$testId];
|
||||
|
||||
// A status this build does not know — a graph written by a newer Pest,
|
||||
// or a corrupt one — is not a result. Returning null re-executes the
|
||||
// test rather than replaying an outcome nobody can interpret.
|
||||
return match ($r['status']) {
|
||||
0 => TestStatus::success(),
|
||||
1 => TestStatus::skipped($r['message']),
|
||||
@@ -717,8 +706,6 @@ final class Graph
|
||||
|
||||
$rel = $this->relative($file);
|
||||
|
||||
// A test file that is no longer on disk cannot be re-run by anyone,
|
||||
// so selecting it would only widen the run for nothing.
|
||||
if ($rel !== null && is_file($this->projectRoot.'/'.$rel)) {
|
||||
$files[$rel] = true;
|
||||
}
|
||||
@@ -727,15 +714,6 @@ final class Graph
|
||||
return array_keys($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a cached result due a re-run names a test file this project
|
||||
* cannot address — an empty path, or one that resolves outside the project
|
||||
* root. Those are genuinely lost, so the caller widens to the full suite.
|
||||
*
|
||||
* A path that resolves fine but is simply absent is *deleted*, not lost:
|
||||
* widening would not run it either, and treating it as unlocated used to
|
||||
* strand `--filtered` on a full replay for good.
|
||||
*/
|
||||
public function hasUnlocatedTestsToRerun(string $branch, ?string $fallbackBranch = null): bool
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
@@ -852,24 +830,6 @@ final class Graph
|
||||
}
|
||||
|
||||
/**
|
||||
* The baseline a read sees for this branch: its own entries layered over the
|
||||
* default branch's, so a key minted by a narrowed run — which only holds the
|
||||
* handful of tests that ran — does not shadow the fallback for everything else.
|
||||
*
|
||||
* Once this branch has had a complete run, the layering becomes per *file*
|
||||
* rather than per test id: the branch's entries for a file it executed are
|
||||
* the whole truth, so the fallback's entries for that same file are dropped
|
||||
* rather than merged. Without that, a test the branch renamed or removed —
|
||||
* and {@see self::pruneStaleResults()} therefore unset — is resurrected by
|
||||
* the default branch on the very next read, and never stops coming back.
|
||||
*
|
||||
* A branch whose key was minted by a *narrowed* run holds only the handful
|
||||
* of tests that ran, and has no business speaking for the rest of their
|
||||
* file, so it keeps the per-test-id merge.
|
||||
*
|
||||
* Read-only: the layering never reaches `$this->baselines`, so writes stay on
|
||||
* the branch that ran.
|
||||
*
|
||||
* @return array{sha: ?string, tree: array<string, string>, complete?: bool, results: array<string, array{status: int, message: string, time: float, assertions?: int, file?: string}>}
|
||||
*/
|
||||
private function baselineFor(string $branch, ?string $fallbackBranch): array
|
||||
@@ -1512,12 +1472,6 @@ final class Graph
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that this branch has run the whole suite at least once, which is
|
||||
* what lets {@see self::baselineFor()} treat its entries as authoritative
|
||||
* for the files they cover. Never mints a key: a run that recorded nothing
|
||||
* has nothing to be authoritative about.
|
||||
*/
|
||||
public function markBaselineComplete(string $branch): void
|
||||
{
|
||||
if (isset($this->baselines[$branch])) {
|
||||
@@ -1525,12 +1479,6 @@ final class Graph
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop this branch's result entries whose test file is no longer on disk.
|
||||
*
|
||||
* Without this nothing but `--fresh` ever reclaims them, and a *failing*
|
||||
* one keeps `--filtered` widened to a full replay on every later run.
|
||||
*/
|
||||
public function pruneResultsForMissingFiles(string $branch): void
|
||||
{
|
||||
if (! isset($this->baselines[$branch]['results'])) {
|
||||
@@ -1561,10 +1509,7 @@ final class Graph
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop baselines for branches git no longer knows, so the graph does not
|
||||
* carry one full copy of the suite per branch ever created.
|
||||
*
|
||||
* @param array<int, string> $keep Branch names that must survive.
|
||||
* @param array<int, string> $keep
|
||||
*/
|
||||
public function pruneMissingBranches(array $keep): void
|
||||
{
|
||||
@@ -1722,11 +1667,6 @@ final class Graph
|
||||
}
|
||||
|
||||
/**
|
||||
* A graph is state on disk that any process may have written: a newer Pest,
|
||||
* a half-finished write, a hand edit. Every branch, every entry and every
|
||||
* field is checked here so that a malformed one is dropped rather than
|
||||
* reaching a read path and taking the run down with it.
|
||||
*
|
||||
* @return array<string, array{sha: ?string, tree: array<string, string>, complete?: bool, results: array<string, array{status: int, message: string, time: float, assertions?: int, file?: string}>}>
|
||||
*/
|
||||
private static function decodeBaselines(mixed $section): array
|
||||
@@ -1738,8 +1678,6 @@ final class Graph
|
||||
$baselines = [];
|
||||
|
||||
foreach ($section as $key => $baseline) {
|
||||
// A branch named `12345` decodes as an integer key, and must not be
|
||||
// mistaken for a malformed one.
|
||||
$branch = (string) $key;
|
||||
|
||||
if ($branch === '') {
|
||||
|
||||
@@ -359,15 +359,6 @@ final class Recorder
|
||||
continue;
|
||||
}
|
||||
|
||||
// A file whose *only* executed line is its last one was included,
|
||||
// not used — the trailing line of an include is all that ran.
|
||||
//
|
||||
// That reading only holds for a driver that reports unexecuted
|
||||
// lines too: pcov returns every executable line (`-1` for the ones
|
||||
// that did not run), so "the single covered line is the highest
|
||||
// line reported" means something. Xdebug reports executed lines
|
||||
// only, where it is true of *any* file that ran a single line —
|
||||
// which is most of them, and dropping those loses the edge.
|
||||
$lineKeys = array_keys($lines);
|
||||
$reportsUnexecutedLines = count($covered) < count($lines);
|
||||
|
||||
|
||||
@@ -38,11 +38,6 @@ final class ResultCollector
|
||||
return;
|
||||
}
|
||||
|
||||
// PHPUnit reports a test that triggered a notice, deprecation or
|
||||
// warning as passed, and emits Passed for it. Recording success here
|
||||
// would erase the issue from the baseline, and a later replay under
|
||||
// --fail-on-deprecation (and friends) would come back green where a
|
||||
// fresh run fails. Keep the issue; only refresh what it cannot know.
|
||||
if (isset($this->triggered[$this->currentTestId])) {
|
||||
$this->refreshTime();
|
||||
|
||||
@@ -120,12 +115,6 @@ final class ResultCollector
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a test was prepared but never finished — the process is being
|
||||
* torn down in the middle of it (an `exit()` inside a test, a killed
|
||||
* worker). What it collected is therefore a partial view of that test
|
||||
* file, and must not license pruning the siblings it never reached.
|
||||
*/
|
||||
public function hasUnfinishedTest(): bool
|
||||
{
|
||||
return $this->currentTestId !== null;
|
||||
@@ -164,11 +153,6 @@ final class ResultCollector
|
||||
$this->startTime = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an issue raised while the test was running. The most important
|
||||
* one wins, exactly as PHPUnit ranks them, so a deprecation does not
|
||||
* shadow the warning that followed it — or the failure.
|
||||
*/
|
||||
private function recordIssue(TestStatus $status): void
|
||||
{
|
||||
if ($this->currentTestId === null) {
|
||||
|
||||
@@ -8,11 +8,6 @@ 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');
|
||||
@@ -164,8 +159,6 @@ test('a project below the git repository root refuses to run and writes nothing'
|
||||
$project = Project::make('master');
|
||||
$nested = $project->nested();
|
||||
|
||||
// git addresses paths from the repository root while the graph is
|
||||
// project-relative, so the two have to coincide. TIA says so and stops.
|
||||
$result = $project->pestIn($nested, '--tia', ...$arguments);
|
||||
|
||||
expect($result->exitCode)->toBe(1, $result->describe())
|
||||
@@ -180,8 +173,6 @@ test('a repository with no commits says so, and leaves plain runs alone', functi
|
||||
$project->git()->run(['checkout', '--quiet', '-b', 'master']);
|
||||
$project->git()->addOrigin();
|
||||
|
||||
// Every git call TIA makes asks about HEAD, which does not exist yet. That
|
||||
// used to surface as `requires "git"`, with git installed and working.
|
||||
$tia = $project->pest('--tia');
|
||||
|
||||
expect($tia->exitCode)->toBe(1, $tia->describe())
|
||||
|
||||
@@ -8,19 +8,6 @@ afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
/*
|
||||
* An active coverage report owns the coverage driver, so TIA cannot open a
|
||||
* session of its own and has to piggyback on PHPUnit's — which is scoped to
|
||||
* `phpunit.xml`'s <source>, not to the whole project. Edges recorded that way
|
||||
* are missing every source file outside that scope, and a change to one of them
|
||||
* would select nothing and replay a pass. Invariant 3 at its most dangerous.
|
||||
*
|
||||
* The rows below assert what a coverage run may and may not leave behind. They
|
||||
* are deliberately silent about exit codes and result counts: `--coverage`
|
||||
* itself fails on an interpreter with no driver, so only the graph's fate is
|
||||
* the same everywhere.
|
||||
*/
|
||||
|
||||
test('a coverage report does not found a dependency graph', function (array $arguments): void {
|
||||
$project = Project::make('master');
|
||||
|
||||
@@ -41,16 +28,12 @@ test('a plain run after a coverage run records the whole project scope', functio
|
||||
|
||||
$graph = $project->graph();
|
||||
|
||||
// Nothing to assert without a driver: there is no graph either way, and the
|
||||
// point of the row is that the *plain* run is the one that founds it.
|
||||
if ($graph === null) {
|
||||
expect($project->graphExists())->toBeFalse();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Self-edges included — they are the first thing a coverage-scoped
|
||||
// recording drops, since test files are not in <source>.
|
||||
expect(array_keys($graph['edges']))->toEqualCanonicalizing(array_keys(Project::EDGES))
|
||||
->and($graph['files'])->toContain('tests/Unit/CalculatorTest.php')
|
||||
->and($graph['files'])->toContain('app/Calculator.php');
|
||||
|
||||
@@ -75,9 +75,6 @@ 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');
|
||||
|
||||
|
||||
@@ -8,11 +8,6 @@ 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');
|
||||
|
||||
@@ -8,13 +8,6 @@ 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
|
||||
|
||||
@@ -9,14 +9,6 @@ afterEach(function (): void {
|
||||
});
|
||||
|
||||
/**
|
||||
* The remote-baseline path is the only one where a graph arrives from another
|
||||
* machine, so every one of these rows is really a hostile-input row: whatever
|
||||
* the artifact carries, the suite still has to run and exit on its own merit.
|
||||
*
|
||||
* `Project::gh()` installs a stand-in for the GitHub CLI, so none of this
|
||||
* touches the network. The graph it serves is a real seeded one, taken out of
|
||||
* the state dir with `detachGraph()` so the run has to fetch it back.
|
||||
*
|
||||
* @param callable(array<string, mixed>): array<string, mixed>|null $mutator
|
||||
* @return array{0: Project, 1: array<string, string>}
|
||||
*/
|
||||
@@ -52,8 +44,6 @@ test('a fetched baseline that will not decode is discarded rather than trusted',
|
||||
|
||||
$result = $project->pestWithEnvironment($project->path(), $environment, '--tia', '--baselined');
|
||||
|
||||
// Nothing may be replayed out of it. Whether the run then records a graph
|
||||
// of its own depends on the coverage driver, so that is not asserted here.
|
||||
expect($result->exitCode)->toBe(0, $result->describe())
|
||||
->and($result->output)->toContain('The dependency graph could not be read')
|
||||
->and($result->tally())->toContain(Project::TOTAL_TESTS.' passed')
|
||||
@@ -120,9 +110,6 @@ test('a network failure warns and lets the suite run', function (string $mode):
|
||||
test('no published baseline yet starts a cooldown, and a corrupt cooldown does not break the run', function (): void {
|
||||
[$project, $environment] = tiaPublishedBaseline('no-runs');
|
||||
|
||||
// On a machine with a coverage driver each run below records a graph of its
|
||||
// own, and a run that has a graph never reaches the fetch at all. Take it
|
||||
// away between runs, so what is under test is the cooldown and nothing else.
|
||||
$discardGraph = function () use ($project): void {
|
||||
if ($project->graphExists()) {
|
||||
$project->detachGraph();
|
||||
|
||||
@@ -8,13 +8,6 @@ afterEach(function (): void {
|
||||
Project::destroyAll();
|
||||
});
|
||||
|
||||
/**
|
||||
* The selection paths below are all driven from a *seeded* graph rather than a
|
||||
* recorded one: Blade and Inertia edges are recorded through Laravel hooks the
|
||||
* fixture project does not have, and a changed `.php` source file would trip
|
||||
* the driverless full-suite fallback. Views and JS files are neither, so what
|
||||
* `Graph::affected()` does with them is measurable on any interpreter.
|
||||
*/
|
||||
function tiaSeedWithView(Project $project, string $view): void
|
||||
{
|
||||
$project->seed('master');
|
||||
@@ -51,8 +44,6 @@ test('a committed rename selects the tests that depended on the old path', funct
|
||||
|
||||
tiaSeedWithView($project, 'resources/views/greeting.blade.php');
|
||||
|
||||
// git reports only the destination of a rename unless asked not to, so the
|
||||
// path the graph holds an edge for is the one that must still show up.
|
||||
$project->git()->run(['mv', 'resources/views/greeting.blade.php', 'resources/views/hello.blade.php']);
|
||||
$project->git()->commit('move the view');
|
||||
$project->snapshot();
|
||||
@@ -70,10 +61,6 @@ test('an affected test file that is gone does not strand a filtered run', functi
|
||||
$project->git()->commit('add view');
|
||||
$project->seed('master');
|
||||
|
||||
// A graph written before this checkout existed — a fetched baseline, or a
|
||||
// branch that deleted the file — can hold an edge for a test file nothing
|
||||
// can run. Selecting it would filter the suite down to nothing and report
|
||||
// success on a change no test looked at.
|
||||
$project->mutateGraph(function (array $graph): array {
|
||||
$id = count($graph['files']);
|
||||
$graph['files'][$id] = 'resources/views/page.blade.php';
|
||||
@@ -174,8 +161,6 @@ test('a changed partial selects the test that rendered its ancestor', function (
|
||||
'resources/views/page.blade.php' => "<x-card>hi</x-card>\n",
|
||||
'resources/views/components/card.blade.php' => "<div>one</div>\n",
|
||||
], 'resources/views/components/card.blade.php'],
|
||||
// Two partials that include each other: the ancestor walk has to notice it
|
||||
// has seen them and stop, rather than chase the cycle forever.
|
||||
'include cycle' => [[
|
||||
'resources/views/page.blade.php' => "@include('partials.a')\n",
|
||||
'resources/views/partials/a.blade.php' => "@include('partials.b')\n",
|
||||
|
||||
@@ -129,8 +129,6 @@ test('the fallback still reaches a branch that has never run a test file', funct
|
||||
|
||||
$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');
|
||||
@@ -212,12 +210,6 @@ test('a malformed baseline entry cannot break the run', function (array $argumen
|
||||
->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');
|
||||
@@ -281,10 +273,6 @@ test('a fatal error mid-file is a test error, not a truncation', function (array
|
||||
->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');
|
||||
@@ -351,16 +339,11 @@ test('a graph whose recorded commit is gone is re-anchored, not warned about for
|
||||
|
||||
$recordedSha = $project->graph()['baselines']['master']['sha'];
|
||||
|
||||
// A rebase, a force-push, a reset: the commit the baseline was recorded at
|
||||
// is no longer an ancestor of HEAD, so nothing can be diffed against it.
|
||||
$project->git()->run(['reset', '--quiet', '--hard', 'HEAD~1']);
|
||||
$project->snapshot();
|
||||
|
||||
$first = $project->pest('--tia', ...$arguments);
|
||||
|
||||
// The whole suite runs, and its results are the truth at HEAD — so the
|
||||
// recorded revision has to move, whether or not a coverage driver was
|
||||
// around to refresh the edges. Without that, the run below repeats forever.
|
||||
expect($first->exitCode)->toBe(0, $first->describe())
|
||||
->and($first->output)->toContain('no longer reachable')
|
||||
->and($first->tally())->toContain(Project::TOTAL_TESTS.' passed')
|
||||
|
||||
@@ -130,10 +130,6 @@ final readonly class GitRepo
|
||||
private function process(array $arguments, bool $mustSucceed): Process
|
||||
{
|
||||
$process = new Process(['git', ...$arguments], $this->path, self::ENV);
|
||||
// Generous on purpose: these rows each spawn a real pest subprocess, so
|
||||
// a loaded machine — a shared CI runner, or two of these suites at once
|
||||
// — can starve a git call for tens of seconds. A timeout here fails the
|
||||
// row for reasons that have nothing to do with what it asserts.
|
||||
$process->setTimeout(120.0);
|
||||
$process->run();
|
||||
|
||||
|
||||
@@ -43,9 +43,6 @@ final class Project
|
||||
public const int TOTAL_TESTS = 6;
|
||||
|
||||
/**
|
||||
* A dataset for the rule that TIA must reach the same outcome sequentially
|
||||
* and in parallel: the same command, run both ways, must leave the same graph.
|
||||
*
|
||||
* @var array<string, array<int, array<int, string>>>
|
||||
*/
|
||||
public const array SEQUENTIAL_AND_PARALLEL = [
|
||||
@@ -124,12 +121,6 @@ final class Project
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A second copy of the fixture app in a subdirectory of this project, so a
|
||||
* run can be started from a root that sits *below* the git repository root.
|
||||
*
|
||||
* @return string The nested project's absolute path.
|
||||
*/
|
||||
public function nested(string $directory = 'nested'): string
|
||||
{
|
||||
$path = $this->path($directory);
|
||||
@@ -179,11 +170,6 @@ final class Project
|
||||
'COLLISION_IGNORE_DURATION' => 'true',
|
||||
'PARATEST' => '0',
|
||||
'PAO_DISABLE' => '1',
|
||||
// Recording is what needs a driver, and recording happens here,
|
||||
// in the subprocess — never in the process running these rows.
|
||||
// Asking for coverage mode only here lets a CI job leave xdebug
|
||||
// off for the suite it is running (whose collection under xdebug
|
||||
// costs more than every scenario put together) and still record.
|
||||
'XDEBUG_MODE' => 'coverage',
|
||||
'HOME' => $this->home(),
|
||||
'GITHUB_EVENT_PATH' => '',
|
||||
@@ -265,10 +251,6 @@ final class Project
|
||||
$sentinel ? $this->sentinel() : $this->snapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the graph out of the state dir and hand back its JSON, so it can be
|
||||
* served as the artifact a remote baseline fetch downloads.
|
||||
*/
|
||||
public function detachGraph(): string
|
||||
{
|
||||
$json = $this->state()->read(Tia::KEY_GRAPH);
|
||||
@@ -287,10 +269,6 @@ final class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a stand-in for the GitHub CLI and return the environment that
|
||||
* points a run at it. `$mode` names the failure it should serve (see
|
||||
* `stubs/gh`); `$payload` is the graph.json its artifact carries.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function gh(string $mode = 'ok', string $payload = '{}'): array
|
||||
@@ -338,10 +316,6 @@ 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 {
|
||||
@@ -390,7 +364,6 @@ final class Project
|
||||
{
|
||||
$baselines = $this->graph()['baselines'] ?? [];
|
||||
|
||||
// A branch named `12345` comes back from json_decode as an integer key.
|
||||
return is_array($baselines) ? array_map(strval(...), array_keys($baselines)) : [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#!/bin/sh
|
||||
# A stand-in for the GitHub CLI, so the remote-baseline path can be exercised
|
||||
# without a network. `GH_STUB_MODE` picks the failure to serve; `GH_STUB_PAYLOAD`
|
||||
# names the graph.json the fake artifact carries.
|
||||
|
||||
if [ "$1" = "auth" ]; then
|
||||
[ "$GH_STUB_MODE" = "unauthenticated" ] && exit 1
|
||||
|
||||
@@ -19,9 +19,6 @@ pest()->in('PHPUnit/GlobPatternTests/SubFolder2/*AsPattern.php')->use(CustomTest
|
||||
|
||||
pest()->in('Visual')->group('integration');
|
||||
|
||||
// Also their own group, so a CI job with a coverage driver can run the TIA
|
||||
// scenarios — the half of TIA that only exists when a driver is present —
|
||||
// without dragging the visual snapshots along.
|
||||
pest()->in('Features/Tia')->group('integration', 'tia');
|
||||
|
||||
// NOTE: global test value container to be mutated and checked across files, as needed
|
||||
|
||||
Reference in New Issue
Block a user