mirror of
https://github.com/pestphp/pest.git
synced 2026-04-21 22:47:27 +02:00
wip
This commit is contained in:
@ -30,7 +30,7 @@ use Throwable;
|
||||
* -----
|
||||
* - **Record** — no graph (or fingerprint / recording commit drifted). The
|
||||
* 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
|
||||
* commit, intersect changed files with graph edges, and run only the
|
||||
* 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
|
||||
* `CallsHandleArguments`. We detect the worker context + recording flag,
|
||||
* 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.
|
||||
*
|
||||
* Guardrails
|
||||
@ -77,29 +77,31 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
/**
|
||||
* State keys under which TIA persists its blobs. Kept here as constants
|
||||
* (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`
|
||||
* run. Stored as bytes so the backend stays JSON/file-agnostic — the
|
||||
* 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
|
||||
* merge. Absent on plain `--coverage` runs so non-TIA usage keeps its
|
||||
* 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.
|
||||
@ -108,7 +110,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
|
||||
@ -16,8 +16,9 @@ use Symfony\Component\Process\Process;
|
||||
*
|
||||
* Storage: **workflow artifacts**, not releases. A dedicated CI workflow
|
||||
* (conventionally `.github/workflows/tia-baseline.yml`) runs the full
|
||||
* suite under `--tia` and uploads the resulting `tia.json` +
|
||||
* `tia-coverage.bin` as a named artifact (`pest-tia-baseline`). On dev
|
||||
* suite under `--tia` and uploads the `.temp/tia/` directory as a named
|
||||
* artifact (`pest-tia-baseline`) containing `graph.json` +
|
||||
* `coverage.bin`. On dev
|
||||
* machines, this class finds the latest successful run of that workflow
|
||||
* and downloads the artifact via `gh`.
|
||||
*
|
||||
@ -48,7 +49,7 @@ final class BaselineSync
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
@ -87,9 +88,7 @@ final class BaselineSync
|
||||
$payload = $this->download($repo);
|
||||
|
||||
if ($payload === null) {
|
||||
$this->output->writeln(
|
||||
' <fg=yellow>TIA</> no baseline published yet — recording locally.',
|
||||
);
|
||||
$this->emitPublishInstructions($repo);
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -110,6 +109,48 @@ final class BaselineSync
|
||||
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
|
||||
* `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
|
||||
* caches share the same location as the rest of Pest's transient
|
||||
* state (PHPUnit result cache, coverage PHP dumps, etc.).
|
||||
* TIA's own subdirectory under Pest's `.temp/`. Keeping every TIA blob
|
||||
* in a single folder (`.temp/tia/`) avoids the `tia-`-prefix salad
|
||||
* 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
|
||||
{
|
||||
@ -42,6 +44,7 @@ final readonly class Bootstrapper implements BootstrapperContract
|
||||
.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
|
||||
* paratest worker partials (`tia-worker-<token>.json`, etc.) without
|
||||
* paratest worker partials (`worker-edges-<token>.json`, etc.) without
|
||||
* exposing backend-specific glob semantics.
|
||||
*
|
||||
* @return list<string>
|
||||
|
||||
@ -16,7 +16,7 @@ final readonly class Fingerprint
|
||||
{
|
||||
// Bump this whenever the set of inputs or the hash algorithm changes, so
|
||||
// older graphs are invalidated automatically.
|
||||
private const int SCHEMA_VERSION = 2;
|
||||
private const int SCHEMA_VERSION = 3;
|
||||
|
||||
/**
|
||||
* @return array<string, int|string|null>
|
||||
@ -26,6 +26,13 @@ final readonly class Fingerprint
|
||||
return [
|
||||
'schema' => self::SCHEMA_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),
|
||||
'composer_lock' => self::hashIfExists($projectRoot.'/composer.lock'),
|
||||
'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> $b
|
||||
|
||||
@ -60,6 +60,10 @@ final readonly class Laravel implements WatchDefault
|
||||
|
||||
// Blade templates — compiled to cache, source file not executed.
|
||||
'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,
|
||||
// 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.
|
||||
'resources/views/livewire/**/*.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.
|
||||
'resources/js/**/*.js' => [$testPath],
|
||||
|
||||
@ -25,9 +25,14 @@ final readonly class Php implements WatchDefault
|
||||
|
||||
return [
|
||||
// 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.testing' => [$testPath],
|
||||
'.env.local' => [$testPath],
|
||||
'.env.*.local' => [$testPath],
|
||||
|
||||
// Docker / CI — can affect integration test infrastructure.
|
||||
'docker-compose.yml' => [$testPath],
|
||||
|
||||
@ -46,7 +46,11 @@ final readonly class Symfony implements WatchDefault
|
||||
'src/Kernel.php' => [$testPath],
|
||||
|
||||
// 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],
|
||||
'src/Migrations/**/*.php' => [$testPath],
|
||||
|
||||
// Twig templates — compiled, source not PHP-executed.
|
||||
'templates/**/*.html.twig' => [$testPath],
|
||||
|
||||
Reference in New Issue
Block a user