mirror of
https://github.com/pestphp/pest.git
synced 2026-04-22 06:57:28 +02:00
wip
This commit is contained in:
@ -30,7 +30,7 @@ use Throwable;
|
|||||||
* -----
|
* -----
|
||||||
* - **Record** — no graph (or fingerprint / recording commit drifted). The
|
* - **Record** — no graph (or fingerprint / recording commit drifted). The
|
||||||
* full suite runs with PCOV / Xdebug capture per test; the resulting
|
* full suite runs with PCOV / Xdebug capture per test; the resulting
|
||||||
* `test → [source_file, …]` edges land in `.temp/tia.json`.
|
* `test → [source_file, …]` edges land in `.temp/tia/graph.json`.
|
||||||
* - **Replay** — graph valid. We diff the working tree against the recording
|
* - **Replay** — graph valid. We diff the working tree against the recording
|
||||||
* commit, intersect changed files with graph edges, and run only the
|
* commit, intersect changed files with graph edges, and run only the
|
||||||
* affected tests. Newly-added tests unknown to the graph are always
|
* affected tests. Newly-added tests unknown to the graph are always
|
||||||
@ -53,7 +53,7 @@ use Throwable;
|
|||||||
* - **Worker, record**: boots through `bin/worker.php`, which re-runs
|
* - **Worker, record**: boots through `bin/worker.php`, which re-runs
|
||||||
* `CallsHandleArguments`. We detect the worker context + recording flag,
|
* `CallsHandleArguments`. We detect the worker context + recording flag,
|
||||||
* activate the `Recorder`, and flush the partial graph on `terminate()`
|
* activate the `Recorder`, and flush the partial graph on `terminate()`
|
||||||
* into `.temp/tia-worker-<TEST_TOKEN>.json`.
|
* into `.temp/tia/worker-edges-<TEST_TOKEN>.json`.
|
||||||
* - **Worker, replay**: nothing to do; args already narrowed.
|
* - **Worker, replay**: nothing to do; args already narrowed.
|
||||||
*
|
*
|
||||||
* Guardrails
|
* Guardrails
|
||||||
@ -77,29 +77,31 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
/**
|
/**
|
||||||
* State keys under which TIA persists its blobs. Kept here as constants
|
* State keys under which TIA persists its blobs. Kept here as constants
|
||||||
* (rather than scattered strings) so the storage layout is visible in
|
* (rather than scattered strings) so the storage layout is visible in
|
||||||
* one place, and so `CoverageMerger` can reference the same keys.
|
* one place, and so `CoverageMerger` can reference the same keys. All
|
||||||
|
* files live under `.temp/tia/` — the `tia-` filename prefix is gone
|
||||||
|
* because the directory already namespaces them.
|
||||||
*/
|
*/
|
||||||
public const string KEY_GRAPH = 'tia.json';
|
public const string KEY_GRAPH = 'graph.json';
|
||||||
|
|
||||||
public const string KEY_AFFECTED = 'tia-affected.json';
|
public const string KEY_AFFECTED = 'affected.json';
|
||||||
|
|
||||||
private const string KEY_WORKER_EDGES_PREFIX = 'tia-worker-edges-';
|
private const string KEY_WORKER_EDGES_PREFIX = 'worker-edges-';
|
||||||
|
|
||||||
private const string KEY_WORKER_RESULTS_PREFIX = 'tia-worker-results-';
|
private const string KEY_WORKER_RESULTS_PREFIX = 'worker-results-';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw-serialised `CodeCoverage` snapshot from the last `--tia --coverage`
|
* Raw-serialised `CodeCoverage` snapshot from the last `--tia --coverage`
|
||||||
* run. Stored as bytes so the backend stays JSON/file-agnostic — the
|
* run. Stored as bytes so the backend stays JSON/file-agnostic — the
|
||||||
* merger un/serialises rather than `require`-ing a PHP file.
|
* merger un/serialises rather than `require`-ing a PHP file.
|
||||||
*/
|
*/
|
||||||
public const string KEY_COVERAGE_CACHE = 'tia-coverage.bin';
|
public const string KEY_COVERAGE_CACHE = 'coverage.bin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker key dropped by `Tia` to tell `Support\Coverage` to apply the
|
* Marker key dropped by `Tia` to tell `Support\Coverage` to apply the
|
||||||
* merge. Absent on plain `--coverage` runs so non-TIA usage keeps its
|
* merge. Absent on plain `--coverage` runs so non-TIA usage keeps its
|
||||||
* current (narrow) behaviour.
|
* current (narrow) behaviour.
|
||||||
*/
|
*/
|
||||||
public const string KEY_COVERAGE_MARKER = 'tia-coverage.marker';
|
public const string KEY_COVERAGE_MARKER = 'coverage.marker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global flag toggled by the parent process so workers know to record.
|
* Global flag toggled by the parent process so workers know to record.
|
||||||
@ -108,7 +110,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Global flag that tells workers to install the TIA filter (replay mode).
|
* Global flag that tells workers to install the TIA filter (replay mode).
|
||||||
* Workers read the affected set from `.temp/tia-affected.json`.
|
* Workers read the affected set from `.temp/tia/affected.json`.
|
||||||
*/
|
*/
|
||||||
private const string REPLAYING_GLOBAL = 'TIA_REPLAYING';
|
private const string REPLAYING_GLOBAL = 'TIA_REPLAYING';
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,9 @@ use Symfony\Component\Process\Process;
|
|||||||
*
|
*
|
||||||
* Storage: **workflow artifacts**, not releases. A dedicated CI workflow
|
* Storage: **workflow artifacts**, not releases. A dedicated CI workflow
|
||||||
* (conventionally `.github/workflows/tia-baseline.yml`) runs the full
|
* (conventionally `.github/workflows/tia-baseline.yml`) runs the full
|
||||||
* suite under `--tia` and uploads the resulting `tia.json` +
|
* suite under `--tia` and uploads the `.temp/tia/` directory as a named
|
||||||
* `tia-coverage.bin` as a named artifact (`pest-tia-baseline`). On dev
|
* artifact (`pest-tia-baseline`) containing `graph.json` +
|
||||||
|
* `coverage.bin`. On dev
|
||||||
* machines, this class finds the latest successful run of that workflow
|
* machines, this class finds the latest successful run of that workflow
|
||||||
* and downloads the artifact via `gh`.
|
* and downloads the artifact via `gh`.
|
||||||
*
|
*
|
||||||
@ -48,7 +49,7 @@ final class BaselineSync
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Artifact name the workflow uploads under. The artifact is a zip
|
* Artifact name the workflow uploads under. The artifact is a zip
|
||||||
* containing `tia.json` (always) + `tia-coverage.bin` (optional).
|
* containing `graph.json` (always) + `coverage.bin` (optional).
|
||||||
*/
|
*/
|
||||||
private const string ARTIFACT_NAME = 'pest-tia-baseline';
|
private const string ARTIFACT_NAME = 'pest-tia-baseline';
|
||||||
|
|
||||||
@ -87,9 +88,7 @@ final class BaselineSync
|
|||||||
$payload = $this->download($repo);
|
$payload = $this->download($repo);
|
||||||
|
|
||||||
if ($payload === null) {
|
if ($payload === null) {
|
||||||
$this->output->writeln(
|
$this->emitPublishInstructions($repo);
|
||||||
' <fg=yellow>TIA</> no baseline published yet — recording locally.',
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -110,6 +109,48 @@ final class BaselineSync
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints actionable instructions for publishing a first baseline when
|
||||||
|
* the consumer-side fetch finds nothing. Without this, the "no
|
||||||
|
* baseline yet" state is a dead-end for users — they see the message
|
||||||
|
* and have to guess what to do next.
|
||||||
|
*/
|
||||||
|
private function emitPublishInstructions(string $repo): void
|
||||||
|
{
|
||||||
|
$this->output->writeln([
|
||||||
|
' <fg=yellow>TIA</> no baseline published yet — recording locally.',
|
||||||
|
'',
|
||||||
|
' To share the baseline with your team, add this workflow to the repo:',
|
||||||
|
'',
|
||||||
|
' <fg=cyan>.github/workflows/tia-baseline.yml</>',
|
||||||
|
'',
|
||||||
|
' name: TIA Baseline',
|
||||||
|
' on:',
|
||||||
|
' push: { branches: [main] }',
|
||||||
|
' schedule: [{ cron: \'0 3 * * *\' }]',
|
||||||
|
' workflow_dispatch:',
|
||||||
|
' jobs:',
|
||||||
|
' baseline:',
|
||||||
|
' runs-on: ubuntu-latest',
|
||||||
|
' steps:',
|
||||||
|
' - uses: actions/checkout@v4',
|
||||||
|
' with: { fetch-depth: 0 }',
|
||||||
|
' - uses: shivammathur/setup-php@v2',
|
||||||
|
' with: { php-version: \'8.4\', coverage: xdebug }',
|
||||||
|
' - run: composer install --no-interaction --prefer-dist',
|
||||||
|
' - run: ./vendor/bin/pest --parallel --tia --coverage',
|
||||||
|
' - uses: actions/upload-artifact@v4',
|
||||||
|
' with:',
|
||||||
|
' name: pest-tia-baseline',
|
||||||
|
' path: vendor/pestphp/pest/.temp/tia/',
|
||||||
|
' retention-days: 30',
|
||||||
|
'',
|
||||||
|
sprintf(' Commit, push, then run once: <fg=cyan>gh workflow run tia-baseline.yml -R %s</>', $repo),
|
||||||
|
' Details: <fg=gray>https://pestphp.com/docs/tia/ci</>',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses `.git/config` for the `origin` remote and extracts
|
* Parses `.git/config` for the `origin` remote and extracts
|
||||||
* `org/repo`. Supports the two URL flavours git emits out of the box.
|
* `org/repo`. Supports the two URL flavours git emits out of the box.
|
||||||
|
|||||||
@ -32,9 +32,11 @@ final readonly class Bootstrapper implements BootstrapperContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve Pest's `.temp/` directory relative to this file so TIA's
|
* TIA's own subdirectory under Pest's `.temp/`. Keeping every TIA blob
|
||||||
* caches share the same location as the rest of Pest's transient
|
* in a single folder (`.temp/tia/`) avoids the `tia-`-prefix salad
|
||||||
* state (PHPUnit result cache, coverage PHP dumps, etc.).
|
* alongside PHPUnit's unrelated files (coverage.php, test-results,
|
||||||
|
* code-coverage/) and makes the CI artifact-upload path a single
|
||||||
|
* directory instead of a list of individual files.
|
||||||
*/
|
*/
|
||||||
private function tempDir(): string
|
private function tempDir(): string
|
||||||
{
|
{
|
||||||
@ -42,6 +44,7 @@ final readonly class Bootstrapper implements BootstrapperContract
|
|||||||
.DIRECTORY_SEPARATOR.'..'
|
.DIRECTORY_SEPARATOR.'..'
|
||||||
.DIRECTORY_SEPARATOR.'..'
|
.DIRECTORY_SEPARATOR.'..'
|
||||||
.DIRECTORY_SEPARATOR.'..'
|
.DIRECTORY_SEPARATOR.'..'
|
||||||
.DIRECTORY_SEPARATOR.'.temp';
|
.DIRECTORY_SEPARATOR.'.temp'
|
||||||
|
.DIRECTORY_SEPARATOR.'tia';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ interface State
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns every key whose name starts with `$prefix`. Used to collect
|
* Returns every key whose name starts with `$prefix`. Used to collect
|
||||||
* paratest worker partials (`tia-worker-<token>.json`, etc.) without
|
* paratest worker partials (`worker-edges-<token>.json`, etc.) without
|
||||||
* exposing backend-specific glob semantics.
|
* exposing backend-specific glob semantics.
|
||||||
*
|
*
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ final readonly class Fingerprint
|
|||||||
{
|
{
|
||||||
// Bump this whenever the set of inputs or the hash algorithm changes, so
|
// Bump this whenever the set of inputs or the hash algorithm changes, so
|
||||||
// older graphs are invalidated automatically.
|
// older graphs are invalidated automatically.
|
||||||
private const int SCHEMA_VERSION = 2;
|
private const int SCHEMA_VERSION = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, int|string|null>
|
* @return array<string, int|string|null>
|
||||||
@ -26,6 +26,13 @@ final readonly class Fingerprint
|
|||||||
return [
|
return [
|
||||||
'schema' => self::SCHEMA_VERSION,
|
'schema' => self::SCHEMA_VERSION,
|
||||||
'php' => PHP_VERSION,
|
'php' => PHP_VERSION,
|
||||||
|
// Loaded extensions + their versions. Guards against the
|
||||||
|
// "recorded without pcov/xdebug → subsequent run has the
|
||||||
|
// driver but graph has no edges" trap where the fingerprint
|
||||||
|
// matches but the graph is effectively empty. Sorted so two
|
||||||
|
// processes with the same extensions in different load order
|
||||||
|
// still produce the same hash.
|
||||||
|
'extensions' => self::extensionsFingerprint(),
|
||||||
'pest' => self::readPestVersion($projectRoot),
|
'pest' => self::readPestVersion($projectRoot),
|
||||||
'composer_lock' => self::hashIfExists($projectRoot.'/composer.lock'),
|
'composer_lock' => self::hashIfExists($projectRoot.'/composer.lock'),
|
||||||
'phpunit_xml' => self::hashIfExists($projectRoot.'/phpunit.xml'),
|
'phpunit_xml' => self::hashIfExists($projectRoot.'/phpunit.xml'),
|
||||||
@ -41,6 +48,27 @@ final readonly class Fingerprint
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic hash of the PHP extension set: `ext-name@version` pairs
|
||||||
|
* sorted alphabetically and joined. Captures both presence (pcov
|
||||||
|
* disappeared? graph must rebuild) and version changes (xdebug minor
|
||||||
|
* bump with coverage-mode semantics).
|
||||||
|
*/
|
||||||
|
private static function extensionsFingerprint(): string
|
||||||
|
{
|
||||||
|
$extensions = get_loaded_extensions();
|
||||||
|
sort($extensions);
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
foreach ($extensions as $name) {
|
||||||
|
$version = phpversion($name);
|
||||||
|
$parts[] = $name.'@'.($version === false ? '?' : $version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash('xxh128', implode("\n", $parts));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $a
|
* @param array<string, mixed> $a
|
||||||
* @param array<string, mixed> $b
|
* @param array<string, mixed> $b
|
||||||
|
|||||||
@ -60,6 +60,10 @@ final readonly class Laravel implements WatchDefault
|
|||||||
|
|
||||||
// Blade templates — compiled to cache, source file not executed.
|
// Blade templates — compiled to cache, source file not executed.
|
||||||
'resources/views/**/*.blade.php' => [$featurePath],
|
'resources/views/**/*.blade.php' => [$featurePath],
|
||||||
|
// Email templates are nested under views/email or views/emails
|
||||||
|
// by convention and power mailable tests that render markup.
|
||||||
|
'resources/views/email/**/*.blade.php' => [$featurePath],
|
||||||
|
'resources/views/emails/**/*.blade.php' => [$featurePath],
|
||||||
|
|
||||||
// Translations — JSON translations read via file_get_contents,
|
// Translations — JSON translations read via file_get_contents,
|
||||||
// PHP translations loaded via include (but during boot).
|
// PHP translations loaded via include (but during boot).
|
||||||
|
|||||||
@ -29,6 +29,10 @@ final readonly class Livewire implements WatchDefault
|
|||||||
// Livewire views live alongside Blade views or in a dedicated dir.
|
// Livewire views live alongside Blade views or in a dedicated dir.
|
||||||
'resources/views/livewire/**/*.blade.php' => [$testPath],
|
'resources/views/livewire/**/*.blade.php' => [$testPath],
|
||||||
'resources/views/components/**/*.blade.php' => [$testPath],
|
'resources/views/components/**/*.blade.php' => [$testPath],
|
||||||
|
// Volt's second default mount — single-file components used as
|
||||||
|
// full-page routes. Missing this means editing a Volt page
|
||||||
|
// doesn't re-run its tests.
|
||||||
|
'resources/views/pages/**/*.blade.php' => [$testPath],
|
||||||
|
|
||||||
// Livewire JS interop / Alpine plugins.
|
// Livewire JS interop / Alpine plugins.
|
||||||
'resources/js/**/*.js' => [$testPath],
|
'resources/js/**/*.js' => [$testPath],
|
||||||
|
|||||||
@ -25,9 +25,14 @@ final readonly class Php implements WatchDefault
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
// Environment files — can change DB drivers, feature flags,
|
// Environment files — can change DB drivers, feature flags,
|
||||||
// queue connections, etc. Not PHP, not fingerprinted.
|
// queue connections, etc. Not PHP, not fingerprinted. Covers
|
||||||
|
// the local-override variants (`.env.local`, `.env.testing.local`)
|
||||||
|
// that both Laravel and Symfony recommend for machine-specific
|
||||||
|
// config.
|
||||||
'.env' => [$testPath],
|
'.env' => [$testPath],
|
||||||
'.env.testing' => [$testPath],
|
'.env.testing' => [$testPath],
|
||||||
|
'.env.local' => [$testPath],
|
||||||
|
'.env.*.local' => [$testPath],
|
||||||
|
|
||||||
// Docker / CI — can affect integration test infrastructure.
|
// Docker / CI — can affect integration test infrastructure.
|
||||||
'docker-compose.yml' => [$testPath],
|
'docker-compose.yml' => [$testPath],
|
||||||
|
|||||||
@ -46,7 +46,11 @@ final readonly class Symfony implements WatchDefault
|
|||||||
'src/Kernel.php' => [$testPath],
|
'src/Kernel.php' => [$testPath],
|
||||||
|
|
||||||
// Migrations — run during setUp (before coverage window).
|
// Migrations — run during setUp (before coverage window).
|
||||||
|
// DoctrineMigrationsBundle's default is `migrations/` at the
|
||||||
|
// project root; many Symfony projects relocate to
|
||||||
|
// `src/Migrations/` — both covered.
|
||||||
'migrations/**/*.php' => [$testPath],
|
'migrations/**/*.php' => [$testPath],
|
||||||
|
'src/Migrations/**/*.php' => [$testPath],
|
||||||
|
|
||||||
// Twig templates — compiled, source not PHP-executed.
|
// Twig templates — compiled, source not PHP-executed.
|
||||||
'templates/**/*.html.twig' => [$testPath],
|
'templates/**/*.html.twig' => [$testPath],
|
||||||
|
|||||||
Reference in New Issue
Block a user