mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 10:52:14 +02:00
wip
This commit is contained in:
113
src/Restarters/PcovRestarter.php
Normal file
113
src/Restarters/PcovRestarter.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Restarters;
|
||||
|
||||
use Pest\Contracts\Restarter;
|
||||
use Pest\Plugins\Tia;
|
||||
|
||||
/**
|
||||
* Re-execs the PHP process with `pcov.directory` pinned to the project
|
||||
* root so pcov never instruments anything outside it (vendor, system
|
||||
* includes, etc.).
|
||||
*
|
||||
* pcov reads `pcov.directory` once, on the first file it instruments —
|
||||
* setting it via `ini_set()` from inside the test runner is too late
|
||||
* for files already compiled by Composer's autoloader. Restarting with
|
||||
* `-dpcov.directory=<root>` means *every* file pcov sees is filtered
|
||||
* correctly.
|
||||
*
|
||||
* Only fires when ALL of these hold:
|
||||
* 1. The pcov extension is loaded.
|
||||
* 2. TIA is enabled for this run (see {@see Tia::isEnabledForRun()} —
|
||||
* either `--tia` on the CLI or `pest()->tia()->always()`); plain
|
||||
* `pest` runs are unaffected.
|
||||
* 3. The current `pcov.directory` differs from the project root.
|
||||
* 4. We are not already the restarted process — guarded by an env
|
||||
* sentinel so a single round-trip is enough.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class PcovRestarter implements Restarter
|
||||
{
|
||||
private const string ENV_RESTARTED = 'PEST_PCOV_RESTARTER_RESTARTED';
|
||||
|
||||
/**
|
||||
* @param array<int, string> $arguments
|
||||
*/
|
||||
public function maybeRestart(string $projectRoot, array $arguments): void
|
||||
{
|
||||
if (! extension_loaded('pcov')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getenv(self::ENV_RESTARTED) === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! Tia::isEnabledForRun($arguments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$desired = $this->normalise($projectRoot);
|
||||
$current = $this->normalise((string) ini_get('pcov.directory'));
|
||||
|
||||
if ($current === $desired) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->restart($projectRoot, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $arguments
|
||||
*/
|
||||
private function restart(string $projectRoot, array $arguments): void
|
||||
{
|
||||
$env = $this->inheritEnv();
|
||||
$env[self::ENV_RESTARTED] = '1';
|
||||
|
||||
$command = array_merge(
|
||||
[PHP_BINARY, '-d', 'pcov.directory='.$projectRoot],
|
||||
array_values($arguments),
|
||||
);
|
||||
|
||||
$proc = @proc_open(
|
||||
$command,
|
||||
[STDIN, STDOUT, STDERR],
|
||||
$pipes,
|
||||
null,
|
||||
$env,
|
||||
);
|
||||
|
||||
if (! is_resource($proc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exitCode = proc_close($proc);
|
||||
|
||||
exit($exitCode === -1 ? 1 : $exitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function inheritEnv(): array
|
||||
{
|
||||
$env = [];
|
||||
|
||||
foreach (getenv() as $name => $value) {
|
||||
if (is_string($name) && is_string($value)) {
|
||||
$env[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $env;
|
||||
}
|
||||
|
||||
private function normalise(string $path): string
|
||||
{
|
||||
return rtrim($path, '/\\');
|
||||
}
|
||||
}
|
||||
130
src/Restarters/XdebugRestarter.php
Normal file
130
src/Restarters/XdebugRestarter.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Restarters;
|
||||
|
||||
use Composer\XdebugHandler\XdebugHandler;
|
||||
use Pest\Contracts\Restarter;
|
||||
use Pest\Plugins\Tia;
|
||||
use Pest\Plugins\Tia\Fingerprint;
|
||||
use Pest\Plugins\Tia\Graph;
|
||||
use Pest\Plugins\Tia\Storage;
|
||||
|
||||
/**
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class XdebugRestarter implements Restarter
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $arguments
|
||||
*/
|
||||
public function maybeRestart(string $projectRoot, array $arguments): void
|
||||
{
|
||||
if (! class_exists(XdebugHandler::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! extension_loaded('xdebug')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->xdebugIsCoverageOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->runLooksDroppable($arguments, $projectRoot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
(new XdebugHandler('pest'))->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* True when Xdebug 3+ is running in coverage-only mode (or empty). False
|
||||
* for older Xdebug without `xdebug_info` — be conservative and leave it
|
||||
* loaded; we can't prove the mode is safe to drop.
|
||||
*/
|
||||
private function xdebugIsCoverageOnly(): bool
|
||||
{
|
||||
if (! function_exists('xdebug_info')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$modes = @xdebug_info('mode');
|
||||
|
||||
if (! is_array($modes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$modes = array_values(array_filter($modes, is_string(...)));
|
||||
|
||||
if ($modes === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $modes === ['coverage'];
|
||||
}
|
||||
|
||||
/**
|
||||
* TIA must be enabled for this run, no coverage flag, no forced
|
||||
* rebuild, and TIA must be about to replay rather than record. Plain
|
||||
* `pest` (and anything else without TIA enabled) keeps Xdebug loaded
|
||||
* so non-TIA users aren't surprised by behaviour changes.
|
||||
*
|
||||
* @param array<int, string> $arguments
|
||||
*/
|
||||
private function runLooksDroppable(array $arguments, string $projectRoot): bool
|
||||
{
|
||||
foreach ($arguments as $value) {
|
||||
if ($value === '--coverage'
|
||||
|| str_starts_with($value, '--coverage=')
|
||||
|| str_starts_with($value, '--coverage-')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($value === '--fresh') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! Tia::isEnabledForRun($arguments)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->tiaWillReplay($projectRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a valid TIA graph already lives on disk AND its structural
|
||||
* fingerprint matches the current environment. Any other outcome
|
||||
* (missing graph, unreadable JSON, structural drift) means TIA will
|
||||
* record and the driver must stay loaded.
|
||||
*/
|
||||
private function tiaWillReplay(string $projectRoot): bool
|
||||
{
|
||||
$path = Storage::tempDir($projectRoot).DIRECTORY_SEPARATOR.Tia::KEY_GRAPH;
|
||||
|
||||
if (! is_file($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$json = @file_get_contents($path);
|
||||
|
||||
if ($json === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$graph = Graph::decode($json, $projectRoot);
|
||||
|
||||
if (! $graph instanceof Graph) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Fingerprint::structuralMatches(
|
||||
$graph->fingerprint(),
|
||||
Fingerprint::compute($projectRoot),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user