chore: style

This commit is contained in:
nuno maduro
2026-08-06 16:02:36 +01:00
parent e90e4f70fc
commit 4d3d0105b7
24 changed files with 52 additions and 728 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ it('does not run user hooks when replaying cached skipped and incomplete results
expect($storage->write(Tia::KEY_GRAPH, (string) $json))->toBeTrue();
$process = new Process(
['php', 'bin/pest', $fixture, '--tia'],
['php', 'bin/pest', '--configuration', 'tests/Fixtures/Suites/TiaReplayHooks.xml', '--tia'],
$projectRoot,
[
'COLLISION_PRINTER' => 'DefaultPrinter',
@@ -4,15 +4,6 @@ declare(strict_types=1);
use Tests\Fixtures\Tia\Project;
/**
* Reading a baseline recorded on another branch.
*
* Without the default-branch fallback, the first `--tia` run on every new branch
* re-runs a whole suite whose results the default branch already holds — once
* per branch, forever, on any repository not named `main`.
*
* @see https://github.com/pestphp/pest/issues/1823
*/
afterEach(function (): void {
Project::destroyAll();
});
@@ -54,8 +45,6 @@ test('replays on a second new branch too', function (): void {
$result = $project->pest('--tia');
// The toll is one full run per new branch. It must not come back for the
// second branch either.
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
->and($result->uncached())->toBe(0, $result->describe());
})->skipOnWindows();
@@ -93,12 +82,6 @@ test('replays inside a worktree on a new branch', function (): void {
$project = Project::make('master');
$worktree = $project->worktree('feature-worktree');
// Seeded against the worktree rather than the main checkout: a worktree's
// `.git` is a file, so `Storage::originIdentity()` cannot read the remote
// from it and the worktree resolves a storage key of its own. That gap is
// separate from the branch fallback, and it is the fallback this row is
// about — the worktree is checked out on a branch the baseline does not
// name, which is the scenario from the issue.
$project->seedFor($worktree, 'master');
$result = $project->pestIn($worktree, '--tia');
@@ -5,17 +5,11 @@ declare(strict_types=1);
use Symfony\Component\Process\ExecutableFinder;
use Tests\Fixtures\Tia\Project;
/**
* How the default branch gets named: declared in `tests/Pest.php`, autodetected
* from the repository, or not answerable at all.
*/
afterEach(function (): void {
Project::destroyAll();
});
test('a declared default branch beats autodetection', function (): void {
// The repository autodetects `develop`, which holds no baseline. Only the
// declaration in `tests/Pest.php` can reach the `master` one.
$project = Project::make('develop', overlay: 'configured-default-branch');
$project->seed('master');
@@ -35,17 +29,12 @@ test('a declared default branch that does not exist degrades to a full run', fun
$result = $project->pest('--tia');
// Nothing to fall back to, so everything runs — and no baseline is minted
// under the name that resolved to nothing.
expect($result->uncached())->toBe(Project::TOTAL_TESTS, $result->describe())
->and($result->exitCode)->toBe(0, $result->describe())
->and($project->branchKeys())->toBe(['master', 'feature-x']);
})->skipOnWindows();
test('the CI provider names the default branch where the checkout cannot', function (): void {
// A CI checkout: `actions/checkout` fetches a single ref instead of cloning,
// so there is no `origin/HEAD` for git to read the default branch from. The
// event payload GitHub hands the job says it outright.
$project = Project::make('master');
$project->git()->unsetOriginHead();
$project->seed('master');
@@ -80,10 +69,6 @@ test('GitLab names the default branch through its own variable', function (): vo
})->skipOnWindows();
test('a lone recorded baseline names the default branch', function (): void {
// Nothing left to ask: no `origin/HEAD`, no CI provider, and an
// `init.defaultBranch` that names a branch this repository does not have.
// The graph holds exactly one baseline, and it is the only one any branch
// could read — so it is the answer.
$project = Project::make('master');
$project->git()->unsetOriginHead();
$project->git()->config('init.defaultBranch', 'main');
@@ -98,9 +83,6 @@ test('a lone recorded baseline names the default branch', function (): void {
})->skipOnWindows();
test('a default branch nothing can name is refused rather than guessed', function (): void {
// Same checkout as above, without the graph that answered it. Guessing here
// is what made this bug expensive: the guess reads no baseline at all, and
// the output calls that a hit.
$project = Project::make('master');
$project->git()->unsetOriginHead();
$project->git()->config('init.defaultBranch', 'main');
@@ -116,9 +98,6 @@ test('a default branch nothing can name is refused rather than guessed', functio
})->skipOnWindows();
test('an init.defaultBranch naming a branch that exists is still trusted', function (): void {
// The setting is the machine's, not the repository's — worth taking only
// where the repository has a branch by that name. It does here, and with no
// graph on disk it is the only source left, so the run must not be refused.
$project = Project::make('master');
$project->git()->unsetOriginHead();
$project->git()->config('init.defaultBranch', 'master');
@@ -141,16 +120,11 @@ test('a repository with no remote is refused rather than silently re-run', funct
$result = $project->pest('--tia');
// Nothing can name the default branch, so every new branch would re-run the
// whole suite with no explanation. Saying so beats doing that quietly. The
// missing remote is the likeliest reason and gets named as such.
expect($result->output)->toContain('Tia mode requires a repository with a remote.')
->and($result->exitCode)->toBe(1, $result->describe());
})->skipOnWindows();
test('a remote-less repository holding one baseline is not refused', function (): void {
// The refusal above exists to stop a guess, not to demand a remote for its
// own sake. With a baseline on disk there is nothing left to guess at.
$project = Project::make('master');
$project->git()->removeOrigin();
@@ -165,8 +139,6 @@ test('a remote-less repository holding one baseline is not refused', function ()
})->skipOnWindows();
test('a declared default branch stands in for a missing remote', function (): void {
// The escape hatch the refusal above points at: with the branch named by
// hand there is nothing left for a remote to answer.
$project = Project::make('master', overlay: 'configured-default-branch');
$project->git()->removeOrigin();
@@ -185,8 +157,6 @@ test('tia still requires git', function (): void {
$result = $project->pest('--tia');
// The soft default-branch resolver runs before the branch is named, and it
// must not swallow this.
expect($result->output)->toContain('The feature "Tia mode" requires "git".')
->and($result->exitCode)->not->toBe(0);
})->skipOnWindows();
@@ -212,8 +182,6 @@ test('the default branch is resolved once per run, not once per test', function
expect($git)->not->toBeNull();
// A `git` first on `PATH` that records what it was asked before handing over
// to the real one.
$log = $project->path('git-calls.log');
$project->write('shim/git', implode("\n", [
'#!/bin/sh',
@@ -4,12 +4,6 @@ declare(strict_types=1);
use Tests\Fixtures\Tia\Project;
/**
* What the fallback is allowed to touch.
*
* Reading another branch's baseline must stay a read: writes belong to the
* branch that ran, and a branch that has no name of its own must not mint one.
*/
afterEach(function (): void {
Project::destroyAll();
});
@@ -20,8 +14,6 @@ test('narrows to the affected tests on a new branch', function (): void {
$project->git()->switchTo('feature-x', new: true);
// Semantic, not cosmetic: PHP is hashed at the AST level, so a comment
// would not register as a change at all.
$project->write('app/Calculator.php', <<<'PHP'
<?php
@@ -50,8 +42,6 @@ test('narrows to the affected tests on a new branch', function (): void {
$result = $project->pest('--tia');
// Two of the three test files cover `Calculator`; the third replays from the
// default branch's baseline.
expect($result->affected())->toBe(4, $result->describe())
->and($result->replayed())->toBe(2, $result->describe())
->and($result->exitCode)->toBe(0, $result->describe());
@@ -60,18 +50,12 @@ test('narrows to the affected tests on a new branch', function (): void {
test('filtered mode reads the fallback too', function (): void {
$project = Project::make('master');
// A failure cached on the default branch. Filtered mode asks the graph which
// test files are due a re-run, and that read has to reach the fallback as
// well: without it a new branch sees nothing to do, reports green, and never
// re-runs the failure — on every subsequent invocation.
$project->seed('master', failing: ['adds two numbers']);
$project->git()->switchTo('feature-x', new: true);
$result = $project->pest('--tia', '--filtered');
// Selection is by file, so the failure's two siblings in `CalculatorTest`
// come along; the other two test files stay out of the run entirely.
expect($result->output)->toContain('from 1 previously unsuccessful test')
->and($result->output)->not->toContain('No affected tests found')
->and($result->tally())->toContain('2 passed');
@@ -99,8 +83,6 @@ test('a detached HEAD replays without minting a branch key', function (): void {
$result = $project->pest('--tia');
// A detached HEAD has no branch of its own. The default branch is the
// honest key, and no phantom one appears beside it.
expect($result->replayed())->toBe(Project::TOTAL_TESTS, $result->describe())
->and($result->uncached())->toBe(0, $result->describe())
->and($project->branchKeys())->toBe(['master']);
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Runs TiaReplayHooks.php as a suite of its own, so `tests/Features/Tia.php` can
replay it without narrowing the run to a path. TIA declines to replay a
narrowed run, and a single-file invocation is exactly that.
-->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
bootstrap="../../../vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="default">
<file>TiaReplayHooks.php</file>
</testsuite>
</testsuites>
</phpunit>
-34
View File
@@ -4,19 +4,10 @@ declare(strict_types=1);
namespace Tests\Fixtures\Tia;
use Pest\Plugins\Tia\Storage;
use RuntimeException;
use Symfony\Component\Process\Process;
/**
* A git repository the scenario tests drive.
*
* Hermetic by construction: every invocation neutralises the machine's global
* and system config and carries its own committer identity. Without that, an
* ambient `init.defaultBranch = main` would answer questions the scenario means
* to leave unanswered, and rows like "resolves to nothing, degrades safely"
* would pass by accident.
*
* @internal
*/
final readonly class GitRepo
@@ -27,9 +18,6 @@ final readonly class GitRepo
public const array ENV = [
'GIT_CONFIG_GLOBAL' => '/dev/null',
'GIT_CONFIG_SYSTEM' => '/dev/null',
// `GIT_CONFIG_SYSTEM` does not cover every system-level file git reads:
// Apple's git also loads one from inside Xcode, and it sets
// `init.defaultBranch`. Only this suppresses all of them.
'GIT_CONFIG_NOSYSTEM' => '1',
'GIT_AUTHOR_NAME' => 'Pest Fixture',
'GIT_AUTHOR_EMAIL' => 'fixture@pestphp.io',
@@ -39,9 +27,6 @@ final readonly class GitRepo
public function __construct(public string $path) {}
/**
* Initialises the repository on `$branch` and commits everything in it.
*/
public function init(string $branch = 'master'): void
{
$this->run(['init', '--quiet']);
@@ -70,12 +55,6 @@ final readonly class GitRepo
$this->run(['checkout', '--quiet', '--detach']);
}
/**
* Registers an `origin`, which also decides the graph's storage key:
* {@see Storage::projectKey()} prefers the remote's
* identity over the path, so two checkouts of one repository — a worktree,
* say — share a single graph.
*/
public function addOrigin(string $url = 'git@github.com:pestphp/tia-fixture.git'): void
{
$this->run(['remote', 'add', 'origin', $url]);
@@ -86,22 +65,12 @@ final readonly class GitRepo
$this->run(['remote', 'remove', 'origin']);
}
/**
* Points `refs/remotes/origin/HEAD` at a local branch — what
* `git remote set-head` would write, without a remote to talk to.
*/
public function setOriginHead(string $branch): void
{
$this->run(['update-ref', 'refs/remotes/origin/'.$branch, 'HEAD']);
$this->run(['symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/'.$branch]);
}
/**
* Drops `refs/remotes/origin/HEAD` while keeping the remote-tracking branch
* — what a CI checkout looks like. `actions/checkout` builds its working
* copy with `git init` plus a single-ref `fetch` rather than a `clone`, and
* only a `clone` writes that symbolic ref.
*/
public function unsetOriginHead(): void
{
$this->run(['symbolic-ref', '--delete', 'refs/remotes/origin/HEAD']);
@@ -112,9 +81,6 @@ final readonly class GitRepo
$this->run(['config', '--local', $key, $value]);
}
/**
* Adds a worktree for a new branch and returns its path.
*/
public function worktree(string $path, string $branch): string
{
$this->run(['worktree', 'add', '--quiet', '-b', $branch, $path]);
-44
View File
@@ -5,20 +5,6 @@ declare(strict_types=1);
namespace Tests\Fixtures\Tia;
/**
* What one run did to the graph.
*
* The three tiers a run may respect, from the conformance matrix:
*
* - COMPLETE — may change everything.
* - RESULTS-ONLY — may change only `baselines[<branch>].results` for tests that
* actually ran. It may never remove an entry, nor move `sha`, `tree`,
* `edges`, `files` or `fingerprint`.
* - HARD-SUPPRESSED — may change nothing at all.
*
* {@see self::writtenCount()} is the load-bearing measurement, and the reason
* {@see Project::sentinel()} exists: without falsified cached values there is no
* way to tell "wrote the same values back" from "wrote nothing".
*
* @internal
*/
final readonly class GraphDelta
@@ -42,9 +28,6 @@ final readonly class GraphDelta
return $this->before !== null && $this->after === null;
}
/**
* Result entries whose stored values actually moved.
*/
public function writtenCount(): int
{
$written = 0;
@@ -71,9 +54,6 @@ final readonly class GraphDelta
return $written;
}
/**
* Result entries that appeared.
*/
public function added(): int
{
$added = 0;
@@ -88,9 +68,6 @@ final readonly class GraphDelta
return $added;
}
/**
* Result entries that were pruned.
*/
public function removed(): int
{
$removed = 0;
@@ -106,9 +83,6 @@ final readonly class GraphDelta
}
/**
* The baseline keys after the run — the headline signal for the
* default-branch rows, where a phantom key is the defect.
*
* @return array<int, string>
*/
public function branchKeys(): array
@@ -129,10 +103,6 @@ final readonly class GraphDelta
return $this->branchKeysBefore() !== $this->branchKeys();
}
/**
* Whether the named branch's baseline is byte-identical — how a row proves
* the fallback is read-only.
*/
public function baselineUntouched(string $branch): bool
{
return ($this->baselines($this->before)[$branch] ?? null)
@@ -149,11 +119,6 @@ final readonly class GraphDelta
return array_any($this->branchKeys(), fn (string $branch) => $this->baselineField($branch, 'tree', $this->before) !== $this->baselineField($branch, 'tree', $this->after));
}
/**
* Compares edges by the file paths they resolve to, not by file id: ids are
* an implementation detail that shifts whenever `files` is rebuilt in a
* different order.
*/
public function edgesMoved(): bool
{
return $this->edgeSets($this->before) !== $this->edgeSets($this->after);
@@ -184,17 +149,11 @@ final readonly class GraphDelta
return $this->branchKeysMoved();
}
/**
* Nothing moved at all.
*/
public function isHardSuppressed(): bool
{
return $this->before === $this->after;
}
/**
* Results may have moved for tests that ran; nothing structural did.
*/
public function isResultsOnly(): bool
{
return ! $this->graphWasCreated()
@@ -206,9 +165,6 @@ final readonly class GraphDelta
&& $this->added() === 0;
}
/**
* A one-line verdict, for failure messages.
*/
public function summary(): string
{
if ($this->graphWasCreated()) {
+2 -29
View File
@@ -5,15 +5,10 @@ declare(strict_types=1);
namespace Tests\Fixtures\Tia;
/**
* The outcome of one `pest` invocation against a fixture project.
*
* @internal
*/
final readonly class PestResult
{
/**
* The run's output, with the terminal's escape sequences taken back out.
*/
public string $output;
/**
@@ -25,39 +20,26 @@ final readonly class PestResult
public int $exitCode,
) {
$this->output = (string) preg_replace([
'#\x1b[[][^A-Za-z]*[A-Za-z]#', // colours, cursor moves
'#\x1b\]8;[^\x1b\x07]*(?:\x1b\\\\|\x07)#', // hyperlinks
'#\x1b[[][^A-Za-z]*[A-Za-z]#',
'#\x1b\]8;[^\x1b\x07]*(?:\x1b\\\\|\x07)#',
], '', $output);
}
/**
* Tests whose cached result was replayed instead of executed.
*/
public function replayed(): int
{
return $this->recapFragment('replayed');
}
/**
* Tests that ran because the graph held nothing for them — the count that
* betrays a fallback which never resolved.
*/
public function uncached(): int
{
return $this->recapFragment('uncached');
}
/**
* Tests that ran because a file they depend on changed.
*/
public function affected(): int
{
return $this->recapFragment('affected');
}
/**
* The `Tests:` summary line, without its label or leading whitespace.
*/
public function tally(): string
{
if (preg_match('/^\s*Tests:\s+(.+)$/m', $this->output, $matches) !== 1) {
@@ -72,10 +54,6 @@ final readonly class PestResult
return str_contains($this->output, $needle);
}
/**
* A description of the run, for failure messages that would otherwise say
* only that 0 !== 6.
*/
public function describe(): string
{
return sprintf(
@@ -86,11 +64,6 @@ final readonly class PestResult
);
}
/**
* Read off the `Tests:` line rather than the whole output: the TIA headline
* counts affected *files*, and matching that instead would be a quietly
* wrong number.
*/
private function recapFragment(string $label): int
{
if (preg_match('/(\d+) '.preg_quote($label, '/').'/', $this->tally(), $matches) !== 1) {
-155
View File
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Tests\Fixtures\Tia;
use FilesystemIterator;
use Pest\Factories\TestCaseFactory;
use Pest\Plugins\Tia;
use Pest\Plugins\Tia\ChangedFiles;
use Pest\Plugins\Tia\FileState;
@@ -19,28 +18,11 @@ use RuntimeException;
use Symfony\Component\Process\Process;
/**
* A throwaway Pest project the TIA scenario tests drive.
*
* Two facts about Pest shape everything here:
*
* 1. `bin/pest` derives the project root from **the autoloader it finds**, not
* from the working directory — `dirname($autoloadPath, 2)`. So the project
* owns a real `vendor/autoload.php` and a real copy of `bin/pest` at the path
* a composer install would have put them. A symlinked `vendor` would resolve
* `__DIR__` straight back to the Pest repository, and every scenario would
* silently measure the wrong project.
* 2. TIA cannot *record* without pcov or Xdebug, and CI has neither. So a
* scenario never records: {@see self::seed()} writes the graph a recording
* run would have written, and the run under test exercises the read path.
*
* @internal
*/
final class Project
{
/**
* Test file → the source files a recording run would have linked it to. The
* self-edge every test file gets is added on top of these.
*
* @var array<string, array<int, string>>
*/
public const array EDGES = [
@@ -50,8 +32,6 @@ final class Project
];
/**
* Test file → the descriptions it declares, in declaration order.
*
* @var array<string, array<int, string>>
*/
public const array TESTS = [
@@ -60,15 +40,9 @@ final class Project
'tests/Feature/CoversCalculatorTest.php' => ['adds within a feature test', 'subtracts within a feature test'],
];
/**
* Every test in the fixture suite.
*/
public const int TOTAL_TESTS = 6;
/**
* Every project scaffolded so far, so a row cannot leak one by failing
* before its own cleanup.
*
* @var array<int, self>
*/
private static array $created = [];
@@ -85,13 +59,6 @@ final class Project
*/
private array $extraPaths = [];
/**
* The root whose graph this project reads and writes.
*
* Its own, except where a row seeds a worktree: a worktree's `.git` is a
* file rather than a directory, so {@see Storage} cannot read the remote
* from it and resolves a storage key of its own.
*/
private string $graphRoot;
private function __construct(public readonly string $path)
@@ -99,14 +66,6 @@ final class Project
$this->graphRoot = $path;
}
/**
* Scaffolds a project whose default branch is `$branch`, and hands it back
* checked out there.
*
* The repository is realistic on purpose: it has an `origin`, and an
* `origin/HEAD` naming `$branch`, which is what a checkout of a real project
* looks like and what the default branch is autodetected from.
*/
public static function make(string $branch = 'master', ?string $overlay = null): self
{
$project = self::scaffold($overlay);
@@ -118,9 +77,6 @@ final class Project
return $project;
}
/**
* Destroys every project scaffolded so far. Belongs in an `afterEach`.
*/
public static function destroyAll(): void
{
while (self::$created !== []) {
@@ -128,14 +84,6 @@ final class Project
}
}
/**
* A project that is not a git repository at all — for the rows that assert
* TIA still demands git, and that a plain run does not care.
*
* Lives in the system temp directory, so it is outside any enclosing
* repository, and owns a real `vendor` rather than a symlinked one, so its
* baseline key cannot collide with another fixture's.
*/
public static function withoutGit(?string $overlay = null): self
{
return self::scaffold($overlay);
@@ -151,12 +99,6 @@ final class Project
return $relative === '' ? $this->path : $this->path.DIRECTORY_SEPARATOR.$relative;
}
/**
* Overwrites a file in the project.
*
* Edits must be semantic: TIA hashes PHP at the AST level, so a
* comment-only change is not a change at all.
*/
public function write(string $relative, string $contents): void
{
$path = $this->path($relative);
@@ -171,14 +113,6 @@ final class Project
}
}
/**
* Adds a worktree for a new branch, scaffolded so `pest` can run in it, and
* returns its path.
*
* The graph is shared with the main checkout, which is the whole point: both
* resolve the same storage key, because {@see Storage} prefers the `origin`
* identity over the path.
*/
public function worktree(string $branch): string
{
$path = $this->path.'-worktree-'.preg_replace('/[^a-z0-9]+/i', '-', $branch);
@@ -190,18 +124,11 @@ final class Project
return $path;
}
/**
* Runs `pest` in the project and returns what happened.
*/
public function pest(string ...$arguments): PestResult
{
return $this->pestIn($this->path, ...$arguments);
}
/**
* Runs `pest` in `$directory` — a worktree, say — against this project's
* graph.
*/
public function pestIn(string $directory, string ...$arguments): PestResult
{
return $this->pestWithEnvironment($directory, [], ...$arguments);
@@ -222,11 +149,6 @@ final class Project
'PARATEST' => '0',
'PAO_DISABLE' => '1',
'HOME' => $this->home(),
// Blanked for the same reason `GitRepo::ENV` blanks git's own
// config: the default branch a CI provider reports is the one
// *its* build is for. Pest's suite runs on GitHub Actions, so
// without this every scenario would autodetect Pest's default
// branch instead of the fixture's.
'GITHUB_EVENT_PATH' => '',
'CI_DEFAULT_BRANCH' => '',
...$environment,
@@ -244,17 +166,6 @@ final class Project
}
/**
* Writes the graph a clean, green recording run on `$branch` would have
* written, and remembers it as the snapshot the next {@see self::delta()}
* compares against.
*
* Sentinelled by default: a row almost always wants to know which entries
* were written, and only a real recording run's values can answer that.
*
* `$failing` names descriptions to record as failures — a clean green run
* can never cache one, so a row that needs a cached failure to re-run has to
* be handed it.
*
* @param array<int, string> $failing
*/
public function seed(string $branch, bool $sentinel = true, array $failing = []): void
@@ -263,9 +174,6 @@ final class Project
}
/**
* Seeds the graph belonging to `$root` — a worktree, say, which resolves a
* storage key of its own.
*
* @param array<int, string> $failing
*/
public function seedFor(string $root, string $branch, bool $sentinel = true, array $failing = []): void
@@ -279,8 +187,6 @@ final class Project
$graph->setFingerprint(Fingerprint::compute($root));
$graph->setRecordedAtSha($branch, $sha);
// Hashes the tree as it stands, so the run under test sees nothing as
// changed — the same call the recording path makes.
$graph->setLastRunTree($branch, $changedFiles->snapshotTree($changedFiles->since($sha) ?? []));
$graph->markKnownTestFiles(array_keys(self::EDGES));
@@ -322,13 +228,6 @@ final class Project
$sentinel ? $this->sentinel() : $this->snapshot();
}
/**
* The id PHPUnit reports for a test in the fixture suite.
*
* Mirrors how Pest names generated test classes
* ({@see TestCaseFactory}): a wrong id here shows up as
* `0 replayed`, which every scenario asserts against.
*/
public static function testId(string $testFile, string $description): string
{
$basename = basename($testFile, '.php');
@@ -343,15 +242,6 @@ final class Project
return 'P\\'.str_replace(DIRECTORY_SEPARATOR, '\\', $relative).'::'.Str::evaluable($description);
}
/**
* Falsifies every cached value, so the next {@see self::delta()} can tell
* "wrote the same values back" from "wrote nothing".
*
* `assertions` is only ever falsified where it is already non-zero: risky,
* skipped and incomplete statuses are *derived* from "performed no
* assertions", so patching those would rewrite the status on replay and
* destroy the very discriminator this exists to provide.
*/
public function sentinel(): void
{
$graph = $this->graph();
@@ -376,8 +266,6 @@ final class Project
}
/**
* The decoded graph, or `null` when there is none.
*
* @return array<string, mixed>|null
*/
public function graph(): ?array
@@ -413,43 +301,21 @@ final class Project
return is_file($this->graphDir().DIRECTORY_SEPARATOR.Tia::KEY_GRAPH);
}
/**
* Remembers the graph as it stands now.
*/
public function snapshot(): void
{
$this->snapshot = $this->graph();
}
/**
* What has happened to the graph since the last snapshot.
*/
public function delta(): GraphDelta
{
return new GraphDelta($this->snapshot, $this->graph());
}
/**
* Writes the `vendor` a composer install would have produced.
*
* Pest is mirrored in at `vendor/pestphp/pest` rather than pointed at,
* because Pest locates things from where its own files sit:
* `bin/pest` finds the project root by walking up from the autoloader it
* loads, and a parallel run picks its worker binary — and with it the
* worker's project root — from the directory the runner class was loaded
* from. Deferring to the repository's copy would resolve both back to the
* Pest repository, and every scenario would quietly measure that instead.
*
* Hardlinked where the filesystem allows it, so the mirror costs almost
* nothing and can never drift from the working tree.
*/
public function scaffoldVendor(string $directory): void
{
$pestRoot = dirname(__DIR__, 3);
$pest = $directory.'/vendor/pestphp/pest';
// `overrides`, `resources` and `stubs` come along because Pest loads
// them relative to `src` — the same list `BootExcludeList` walks.
foreach (['src', 'overrides', 'resources', 'stubs'] as $tree) {
self::mirror($pestRoot.'/'.$tree, $pest.'/'.$tree);
}
@@ -460,21 +326,11 @@ final class Project
self::mirror($pestRoot.'/composer.json', $pest.'/composer.json');
// Pest's own autoloader, with the mirrored copy taking precedence: the
// repository's `vendor` supplies PHPUnit, Symfony and the plugin
// packages, none of which care where they are loaded from.
file_put_contents($directory.'/vendor/autoload.php', sprintf(
"<?php\n\n\$loader = require %s;\n\$loader->addPsr4('Pest\\\\', __DIR__.'/pestphp/pest/src', true);\n\nreturn \$loader;\n",
var_export($pestRoot.'/vendor/autoload.php', true),
));
// Invoking the binary directly skips composer's bin proxy, which is
// what would otherwise define `$GLOBALS['_composer_bin_dir']`. Without
// it `Pest\Plugin\Loader` looks for `vendor/bin/../pest-plugins.json`
// relative to the working directory — so that is where the plugin list
// goes, and mirroring the repository's keeps it in step with
// composer.json. `vendor/bin` has to exist for the `..` in that path to
// resolve, empty though it is.
if (! is_dir($directory.'/vendor/bin') && ! @mkdir($directory.'/vendor/bin', 0755, true)) {
throw new RuntimeException(sprintf('Unable to create [%s].', $directory.'/vendor/bin'));
}
@@ -500,9 +356,6 @@ final class Project
}
/**
* `Storage` reads `HOME` from the environment, so the graph lands inside the
* throwaway project instead of the developer's real `~/.pest`.
*
* @template TReturn
*
* @param callable(): TReturn $callback
@@ -529,8 +382,6 @@ final class Project
throw new RuntimeException(sprintf('Unable to create [%s].', $path));
}
// Realpathed because Pest realpaths its own project root, and macOS
// hands out a symlinked temp directory.
$real = realpath($path);
$project = new self($real === false ? $path : $real);
@@ -543,8 +394,6 @@ final class Project
self::copy(__DIR__.'/overlays/'.$overlay, $project->path);
}
// `ChangedFiles` asks git what changed, so anything the scaffold writes
// but the project does not own has to be invisible to it.
$project->write('.gitignore', implode("\n", ['/vendor/', '/.home/', '/.phpunit.cache/', '']));
$project->scaffoldVendor($project->path);
@@ -553,10 +402,6 @@ final class Project
return $project;
}
/**
* Mirrors a file or directory, hardlinking where the filesystem allows it
* and copying where it does not.
*/
private static function mirror(string $from, string $to): void
{
if (is_dir($from)) {
@@ -4,8 +4,6 @@ declare(strict_types=1);
use Fixture\App\Calculator;
// A second test file over the same source file, so a `Calculator` edit narrows
// to two of the three test files rather than to one.
test('adds within a feature test', function (): void {
expect((new Calculator)->add(10, 5))->toBe(15);
});
-3
View File
@@ -2,8 +2,5 @@
declare(strict_types=1);
// The fixture project has no composer autoloader of its own — its `vendor/`
// holds nothing but a shim pointing back at Pest's. Requiring the two classes
// here is enough for every test file in the suite.
require_once __DIR__.'/../app/Calculator.php';
require_once __DIR__.'/../app/Greeter.php';
@@ -4,9 +4,6 @@ declare(strict_types=1);
use Fixture\App\Calculator;
// `test()` rather than `it()`: the harness seeds results by test id, and `it()`
// would prefix every description with `it `, leaving Project::TESTS a step away
// from what is written here.
test('adds two numbers', function (): void {
expect((new Calculator)->add(1, 2))->toBe(3);
});
@@ -5,7 +5,4 @@ declare(strict_types=1);
require_once __DIR__.'/../app/Calculator.php';
require_once __DIR__.'/../app/Greeter.php';
// Declared default branch. Wins over whatever the repository autodetects — the
// escape hatch for a checkout with no `origin/HEAD` and a misleading
// `init.defaultBranch`.
pest()->tia()->defaultBranch('master');
@@ -5,6 +5,4 @@ declare(strict_types=1);
require_once __DIR__.'/../app/Calculator.php';
require_once __DIR__.'/../app/Greeter.php';
// A branch the repository does not have. Accepted as configured — a name that
// resolves to no baseline degrades to a full run, which is safe.
pest()->tia()->defaultBranch('nope');
-2
View File
@@ -19,8 +19,6 @@ pest()->in('PHPUnit/GlobPatternTests/SubFolder2/*AsPattern.php')->use(CustomTest
pest()->in('Visual')->group('integration');
// Every row scaffolds a throwaway project and runs `pest` in it as a
// subprocess, which is far too slow for the unit suite.
pest()->in('Features/Tia')->group('integration');
// NOTE: global test value container to be mutated and checked across files, as needed
-2
View File
@@ -67,8 +67,6 @@ describe('applyMigrationChanges()', function (): void {
describe('rerun tracking', function (): void {
beforeEach(function (): void {
// `hasUnlocatedTestsToRerun()` stats each recorded file to tell a
// deleted test apart from a live one, so the files have to exist.
$this->projectRoot = sys_get_temp_dir().'/pest-tia-rerun-'.bin2hex(random_bytes(4));
mkdir($this->projectRoot.'/tests/Feature', 0755, true);