diff --git a/.github/workflows/functional.yml b/.github/workflows/functional.yml new file mode 100644 index 0000000..6e579f1 --- /dev/null +++ b/.github/workflows/functional.yml @@ -0,0 +1,69 @@ +name: Functional Tests + +on: + push: + paths-ignore: ["**.md"] + pull_request: + paths-ignore: ["**.md"] + +env: + COMPOSER_FLAGS: --ansi --no-interaction --no-progress --prefer-dist + +jobs: + tests: + name: Functional Tests + + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + + strategy: + matrix: + php-version: + - "7.2" + - "8.3" + os: [ubuntu-latest] + experimental: [false] + include: + - php-version: "7.2" + os: windows-latest + experimental: false + - php-version: "8.3" + os: windows-latest + experimental: false + - php-version: "8.4" + os: ubuntu-latest + experimental: true + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + coverage: xdebug + php-version: ${{ matrix.php-version }} + + - name: Get composer cache directory + id: composercache + shell: bash + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Restore cached dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composercache.outputs.dir }} + key: php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: php-${{ matrix.php-version }}-composer- + + - name: Install latest dependencies + run: composer update ${{ env.COMPOSER_FLAGS }} + + - name: Run tests + if: ${{ !matrix.experimental }} + run: vendor/bin/phpunit --group functional + + # Show deprecations on PHP 8.4 + - name: Run tests (experimental) + if: ${{ matrix.experimental }} + run: vendor/bin/phpunit --group functional --display-deprecations diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8fce37b..717aec4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,4 +8,10 @@ tests + + + + functional + + diff --git a/tests/App/Framework/AppHelper.php b/tests/App/Framework/AppHelper.php new file mode 100644 index 0000000..9a4f4e0 --- /dev/null +++ b/tests/App/Framework/AppHelper.php @@ -0,0 +1,185 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +use Composer\XdebugHandler\Process; +use Composer\XdebugHandler\XdebugHandler; + +class AppHelper extends AppRunner +{ + public const ENV_IS_DISPLAY = 'XDEBUG_HANDLER_TEST_DISPLAY'; + public const ENV_SHOW_INIS = 'XDEBUG_HANDLER_TEST_INIS'; + + /** @var string */ + private $appName; + /** @var string */ + private $envPrefix; + /** @var non-empty-list */ + private $serverArgv; + /** @var class-string|null */ + private $className; + /** @var bool */ + private $display; + /** @var bool */ + private $showInis; + + /** @var Logger */ + private $logger; + + /** @var Output */ + private $output; + + /** @var Status */ + private $status = null; + + public function __construct(string $script) + { + parent::__construct(dirname($script)); + $this->appName = $this->getAppName($script); + $this->envPrefix = $this->getEnvPrefix($script); + + $this->display = $this->getDisplayFromServer(); + $this->showInis = $this->getInisFromServer(); + $this->setServerArgv(); + + $this->output = new Output($this->display); + $this->logger = new Logger($this->output); + $this->status = new Status($this->display, $this->showInis); + } + + /** + * + * @return non-empty-list + */ + public function getServerArgv(): array + { + return $this->serverArgv; + } + + public function getServerArgv0(): string + { + return $this->getServerArgv()[0]; + } + + /** + * + * @param class-string $class + * @param array $settings + */ + public function getXdebugHandler(?string $class = null, ?array $settings = null): XdebugHandler + { + if ($class === null) { + $class = XdebugHandler::class; + } elseif (!is_subclass_of($class, XdebugHandler::class)) { + throw new \RuntimeException($class.' must extend XdebugHandler'); + } + + $this->className = $class; + + /** @var XdebugHandler $xdebug */ + $xdebug = new $class($this->envPrefix); + $xdebug->setLogger($this->logger); + + if (isset($settings['mainScript'])) { + $xdebug->setMainScript($settings['mainScript']); + } + + if (isset($settings['persistent'])) { + $xdebug->setPersistent(); + } + + return $xdebug; + } + + public function runScript(string $script, ?PhpOptions $options = null): void + { + parent::runScript($script, $options); + } + + public function write(string $message): void + { + $this->logger->write($message, $this->appName); + } + + public function writeXdebugStatus(): void + { + $className = $this->className ?? XdebugHandler::class; + $items = $this->status->getWorkingsStatus($className); + + foreach($items as $item) { + $this->write('working '.$item); + } + } + + private function setServerArgv(): void + { + $args = []; + $errors = false; + + if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) { + foreach ($_SERVER['argv'] as $value) { + if (!is_string($value)) { + $errors = true; + break; + } + + $args[] = $value; + } + } + + if ($errors || count($args) === 0) { + throw new \RuntimeException('$_SERVER[argv] is not as expected'); + } + + $this->serverArgv = $args; + } + + private function getDisplayFromServer(): bool + { + $result = false; + + if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) { + $key = array_search('--display', $_SERVER['argv'], true); + + if ($key !== false) { + $result = true; + Process::setEnv(self::ENV_IS_DISPLAY, '1'); + unset($_SERVER['argv'][$key]); + } else { + $result = false !== getenv(self::ENV_IS_DISPLAY); + } + } + + return $result; + } + + private function getInisFromServer(): bool + { + $result = false; + + if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) { + $key = array_search('--inis', $_SERVER['argv'], true); + + if ($key !== false) { + $result = true; + Process::setEnv(self::ENV_SHOW_INIS, '1'); + unset($_SERVER['argv'][$key]); + } else { + $result = false !== getenv(self::ENV_SHOW_INIS); + } + } + + return $result; + } +} diff --git a/tests/App/Framework/AppRunner.php b/tests/App/Framework/AppRunner.php new file mode 100644 index 0000000..9e123eb --- /dev/null +++ b/tests/App/Framework/AppRunner.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +class AppRunner +{ + /** @var string */ + private $scriptDir; + /** @var PhpExecutor */ + private $phpExecutor; + + public function __construct(?string $scriptDir = null) + { + $this->scriptDir = (string) realpath($scriptDir ?? __DIR__); + + if (!is_dir($this->scriptDir)) { + throw new \RuntimeException('Directory does not exist: '.$this->scriptDir); + } + + $this->phpExecutor = new PhpExecutor(); + } + + public function run(string $script, ?PhpOptions $options = null, bool $allow = false): Logs + { + $script = $this->checkScript($script); + + if ($options === null) { + $options = new PhpOptions(); + } + + // enforce output + $options->setPassthru(false); + + if ($allow) { + $options->addEnv($this->getEnvAllow($script), '1'); + } + + $output = $this->phpExecutor->run($script, $options, $this->scriptDir); + + $lines = preg_split("/\r\n|\n\r|\r|\n/", trim($output)); + $outputLines = $lines !== false ? $lines : []; + + return new Logs($outputLines); + } + + public function runScript(string $script, ?PhpOptions $options = null): void + { + $script = $this->checkScript($script); + + if ($options === null) { + $options = new PhpOptions(); + } + + // set passthru in child proccesses so output gets collected + $options->setPassthru(true); + $this->phpExecutor->run($script, $options, $this->scriptDir); + } + + public function getAppName(string $script): string + { + return basename($script, '.php'); + } + + public function getEnvAllow(string $script): string + { + return sprintf('%s_ALLOW_XDEBUG', $this->getEnvPrefix($script)); + } + + public function getEnvPrefix(string $script): string + { + $name = $this->getAppName($script); + + return strtoupper(str_replace(array('-', ' '), '_', $name)); + } + + private function checkScript(string $script): string + { + if (file_exists($script)) { + return $script; + } + + $path = $this->scriptDir.'/'.$script; + + if (file_exists($path)) { + return $path; + } + + throw new \RuntimeException('File does not exist: '.$script); + } +} diff --git a/tests/App/Framework/LogItem.php b/tests/App/Framework/LogItem.php new file mode 100644 index 0000000..d3ec6ca --- /dev/null +++ b/tests/App/Framework/LogItem.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +class LogItem +{ + /** @var int */ + public $pid; + /** @var string */ + public $name; + /** @var string */ + public $value; + + public function __construct(int $pid, string $name, string $value) + { + $this->pid = $pid; + $this->name = $name; + $this->value = $value; + } +} diff --git a/tests/App/Framework/Logger.php b/tests/App/Framework/Logger.php new file mode 100644 index 0000000..a0fb1b5 --- /dev/null +++ b/tests/App/Framework/Logger.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +use Psr\Log\AbstractLogger; + +class Logger extends AbstractLogger +{ + /** @var Output $output */ + private $output; + + public function __construct(Output $output) + { + $this->output = $output; + } + + /** + * @inheritdoc + * @phpstan-param mixed[] $context + */ + public function log($level, $message, array $context = array()): void + { + $this->write((string) $message, Logs::LOGGER_NAME); + } + + public function write(string $message, string $name): void + { + $this->output->write($message, $name); + } +} diff --git a/tests/App/Framework/Logs.php b/tests/App/Framework/Logs.php new file mode 100644 index 0000000..148f671 --- /dev/null +++ b/tests/App/Framework/Logs.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +class Logs +{ + public const LOGGER_NAME = 'logger'; + + /** @var array> */ + private $items = []; + + /** + * + * @param list $outputLines + */ + public function __construct(array $outputLines) + { + $pids = []; + + foreach ($outputLines as $line) { + $line = trim($line); + + if (!(bool) preg_match('/^(.+)\\[(\\d+)\\](.+)$/', $line, $matches)) { + continue; + } + + $pid = (int) $matches[2]; + $item = new LogItem($pid, $matches[1], $matches[3]); + + if (!isset($pids[$pid])) { + $this->items[] = []; + $pids[$pid] = count($this->items) - 1; + } + + $index = $pids[$pid]; + $this->items[$index][] = $item; + } + } + + /** + * + * @return list + */ + public function getOutputForProcess(int $seqNo): array + { + $index = $seqNo - 1; + + if (isset($this->items[$index])) { + return $this->items[$index]; + } + + return []; + } + + /** + * + * @return list + */ + public function getValuesForProcess(int $seqNo, bool $forLogger = true): array + { + $items = $this->getOutputForProcess($seqNo); + + if ($forLogger) { + $result = $this->filterValuesForLogger($items); + } else { + $result = $this->filterValuesForScript($items); + } + + return $result; + } + + /** + * + * @return list + */ + public function getValuesForProcessName(int $seqNo, string $name): array + { + $items = $this->getOutputForProcess($seqNo); + + return $this->filterValuesForScriptName($items, $name); + } + + /** + * + * @param list $items + */ + public function getItemFromList(array $items, int $seqNo, bool $complete = false): string + { + $index = $seqNo - 1; + + if (!isset($items[$index])) { + throw new \LogicException('Log item not found at index: '.$index); + } + + $item = $items[$index]; + + if (!$complete) { + return $item->value; + } + + return sprintf('%s[%d] %s', $item->name, $item->pid, $item->value); + } + + /** + * + * @param list $items + * @return list + */ + private function filterValuesForScript(array $items): array + { + $result = array_filter($items, function($item) { + return $item->name !== self::LOGGER_NAME; + }); + + return array_values($result); + } + + /** + * + * @param list $items + * @return list + */ + private function filterValuesForScriptName(array $items, string $name): array + { + $result = array_filter($items, function($item) use ($name) { + return $item->name === $name; + }); + + return array_values($result); + } + + /** + * + * @param list $items + * @return list + */ + private function filterValuesForLogger(array $items): array + { + $result = array_filter($items, function($item) { + return $item->name === 'logger'; + }); + + return array_values($result); + } +} diff --git a/tests/App/Framework/Output.php b/tests/App/Framework/Output.php new file mode 100644 index 0000000..07e1d41 --- /dev/null +++ b/tests/App/Framework/Output.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +use Composer\Pcre\Preg; +use Composer\XdebugHandler\Process; + +class Output +{ + public const ENV_OUTPUT_INDENT = 'XDEBUG_HANDLER_TEST_INDENT'; + + /** @var bool */ + private $isDisplay; + + /** @var int */ + private $indent = 0; + + public function __construct(bool $display) + { + $this->isDisplay = $display; + + if (!defined('STDOUT')) { + define('STDOUT', fopen('php://stdout', 'w')); + } + + if (false === ($indent = getenv(self::ENV_OUTPUT_INDENT))) { + Process::setEnv(self::ENV_OUTPUT_INDENT, '0'); + } else { + $this->indentIncrease(); + } + } + + public function __destruct() + { + $this->indentDecrease(); + } + + public function write(string $message, string $name): void + { + $prefix = sprintf('%s[%d]', $name, getmypid()); + $color = $this->isDisplay && $name !== Logs::LOGGER_NAME; + $text = $this->format($prefix, $message, $color); + + fwrite(STDOUT, $text); + fflush(STDOUT); + } + + private function format(string $prefix, string $text, bool $color): string + { + if ($color) { + $prefix = sprintf("\033[33;33m%s\033[0m", $prefix); + + if (Preg::isMatch('/(^working|^initial)/', $text, $matches)) { + $info = substr($text, 7); + $text = sprintf("\033[0;32m%s\033[0m", $matches[1]); + $text .= sprintf("\033[0;93m%s\033[0m", $info); + } else { + $text = sprintf("\033[0;32m%s\033[0m", $text); + } + } + + $text = sprintf('%s %s%s', $prefix, $text, PHP_EOL); + + if ($this->indent > 0) { + $prefix = str_repeat(chr(32), $this->indent); + $text = $prefix.$text; + } + + return $text; + } + + private function indentDecrease(): void + { + if (false !== ($indent = getenv(self::ENV_OUTPUT_INDENT))) { + $this->indent = intval($indent) - 2; + Process::setEnv(self::ENV_OUTPUT_INDENT, (string) $this->indent); + } + } + + private function indentIncrease(): void + { + if (false !== ($indent = getenv(self::ENV_OUTPUT_INDENT))) { + $this->indent = intval($indent) + 2; + Process::setEnv(self::ENV_OUTPUT_INDENT, (string) $this->indent); + } + } +} diff --git a/tests/App/Framework/PhpExecutor.php b/tests/App/Framework/PhpExecutor.php new file mode 100644 index 0000000..fafa079 --- /dev/null +++ b/tests/App/Framework/PhpExecutor.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +use Composer\XdebugHandler\Process; + +class PhpExecutor +{ + public function run(string $script, PhpOptions $options, ?string $cwd = null): string + { + $content = null; + $params = array_merge([PHP_BINARY], $options->getPhpArgs()); + + if ($options->getStdin()) { + $content = $this->getContents($script); + } else { + $params[] = $script; + } + + $params = array_merge($params, $options->getScriptArgs()); + + $this->setEnvs($options->getEnvs()); + + try { + $output = $this->execute($params, $cwd, $content, $options->getPassthru()); + } finally { + $this->unsetEnvs($options->getEnvs()); + } + + return $output; + } + + /** + * + * @param non-empty-list $args + */ + private function execute(array $args, ?string $cwd, ?string $stdin, bool $passthru): string + { + $command = $this->getCommand($args); + $output = ''; + $fds = []; + + if ($stdin !== null) { + $fds[0] = ['pipe', 'r']; + } + + if (!$passthru) { + $fds[1] = ['pipe', 'wb']; + } + + $process = proc_open($command, $fds, $pipes, $cwd); + + if (is_resource($process)) { + if ($stdin !== null) { + fwrite($pipes[0], $stdin); + fclose($pipes[0]); + } + + if (!$passthru) { + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + } + + $exitCode = proc_close($process); + } + + return (string) $output; + } + + /** + * + * @param non-empty-list $args + * @return non-empty-list|string + */ + private function getCommand(array $args) + { + if (PHP_VERSION_ID >= 70400) { + return $args; + } + + $command = Process::escapeShellCommand($args); + + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + // Outer quotes required on cmd string below PHP 8 + $command = '"'.$command.'"'; + } + + return $command; + } + + private function getContents(string $file): string + { + $result = file_get_contents($file); + + if ($result !== false) { + return $result; + } + + throw new \RuntimeException('Unable to read file: '.$file); + } + + /** + * + * @param array $envs + */ + private function setEnvs(array $envs): void + { + foreach ($envs as $name => $value) { + Process::setEnv($name, $value); + } + } + + /** + * + * @param array $envs + */ + private function unsetEnvs(array $envs): void + { + foreach ($envs as $name => $value) { + Process::setEnv($name); + } + } +} diff --git a/tests/App/Framework/PhpOptions.php b/tests/App/Framework/PhpOptions.php new file mode 100644 index 0000000..9c7e383 --- /dev/null +++ b/tests/App/Framework/PhpOptions.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +class PhpOptions +{ + /** @var list */ + private $phpArgs = []; + /** @var list */ + private $scriptArgs = []; + /** @var array */ + private $envs = []; + /** @var bool */ + private $stdin = false; + /** @var bool */ + private $passthru = false; + + /** + * @return list + */ + public function getPhpArgs(): array + { + return $this->phpArgs; + } + + /** + * @return list + */ + public function getScriptArgs(): array + { + return $this->scriptArgs; + } + + /** + * @return array + */ + public function getEnvs(): array + { + return $this->envs; + } + + public function getStdin(): bool + { + return $this->stdin; + } + + public function getPassthru(): bool + { + return $this->passthru; + } + + /** + * + * @param string ...$phpArgs + */ + public function addPhpArgs(...$phpArgs): void + { + foreach ($phpArgs as $item) { + $this->phpArgs[] = $item; + } + } + + /** + * + * @param string ...$scriptArgs + */ + public function addScriptArgs(...$scriptArgs): void + { + foreach ($scriptArgs as $item) { + $this->scriptArgs[] = $item; + } + } + + public function addEnv(string $name, string $value): void + { + $this->envs[$name] = $value; + } + + public function setPassthru(bool $value): void + { + $this->passthru = $value; + } + + public function setStdin(bool $value): void + { + $this->stdin = $value; + } +} diff --git a/tests/App/Framework/Status.php b/tests/App/Framework/Status.php new file mode 100644 index 0000000..b95a68f --- /dev/null +++ b/tests/App/Framework/Status.php @@ -0,0 +1,227 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Framework; + +use Composer\XdebugHandler\XdebugHandler; + +/** + * @phpstan-import-type restartData from \Composer\XdebugHandler\PhpConfig + */ +class Status +{ + private const INVALID = 'not applicable'; + private const MISSING = 'not available'; + + /** @var bool */ + private $isDisplay; + /** @var bool */ + private $showInis; + /** @var bool */ + private $loaded = false; + + public function __construct(bool $display, bool $showInis) + { + $this->isDisplay = $display; + $this->showInis = $showInis; + } + + /** + * + * @param class-string $className + * @return list + */ + public function getWorkingsStatus(string $className): array + { + $this->loaded = extension_loaded('xdebug'); + $result = []; + + // status + [$status, $info] = $this->getXdebugStatus(); + $result[] = sprintf('[%s] %s', $status, $info); + + if (!$this->isDisplay) { + return $result; + } + + // active + $active = $this->getActive($className); + $result[] = sprintf('[active] %s', $active); + + // skipped + $skipped = $this->getSkipped($className); + $result[] = sprintf('[skipped] %s', $skipped); + + // envs + foreach($this->getEnvs() as $item) { + $result[] = sprintf('[env] %s', $item); + } + + // inis + foreach($this->getInis($className) as $item) { + $result[] = sprintf('[ini] %s', $item); + } + + // settings + foreach($this->getSettings($className) as $item) { + $result[] = sprintf('[settings] %s', $item); + } + + return $result; + } + + /** + * + * @return array{0: string, 1: string} + */ + private function getXdebugStatus(): array + { + $status = $this->loaded ? 'XDEBUG' : 'NO XDEBUG'; + $text = 'The Xdebug extension is'; + $suffix = $this->loaded ? 'loaded' : 'not loaded'; + $info = sprintf('%s %s', $text, $suffix); + + return [$status, $info]; + } + + /** + * + * @param class-string $className + */ + private function getActive(string $className): string + { + $active = $className::isXdebugActive(); + + return $active === true ? 'true' : 'false'; + } + + /** + * + * @param class-string $className + */ + private function getSkipped(string $className): string + { + $version = $className::getSkippedVersion(); + + if ($version === '') { + return $this->loaded ? self::INVALID : self::MISSING; + } + + return $version; + } + + /** + * @return list + */ + private function getEnvs(): array + { + $result = []; + $envs = ['PHPRC', 'PHP_INI_SCAN_DIR']; + + foreach ($envs as $name) { + $format = '%s=%s'; + + if (false === ($env = getenv($name))) { + $env = 'unset'; + $format = '%s %s'; + } elseif ($env === '') { + $env = "''"; + } + + $result[] = sprintf($format, $name, $env); + } + + return $result; + } + + /** + * @param class-string $className + * @return list + */ + private function getInis(string $className): array + { + $iniFiles = $this->findIniFiles($className); + + if ($iniFiles[0] === '') { + array_shift($iniFiles); + } + + $count = count($iniFiles); + + if ($this->showInis || $count === 1) { + return $iniFiles; + } + + $message = sprintf("%d more ini files (show with '--inis' option)", $count - 1); + + return [$iniFiles[0], $message]; + } + + /** + * @param class-string $className + * @return list + */ + private function findIniFiles(string $className): array + { + $extended = $className !== XdebugHandler::class; + + if ($extended) { + $iniFiles = $className::getAllIniFiles(); + } elseif (class_exists(XdebugHandler::class)) { + $iniFiles = $className::getAllIniFiles(); + } else { + $iniFiles = array((string) php_ini_loaded_file()); + $scanned = php_ini_scanned_files(); + + if ($scanned !== false) { + $iniFiles = array_merge($iniFiles, array_map('trim', explode(',', $scanned))); + } + } + + // @phpstan-ignore-next-line + return !is_array($iniFiles) ? [self::MISSING] : $iniFiles; + } + + /** + * + * @param class-string $className + * @return list + */ + private function getSettings(string $className): array + { + /** @var restartData|null $settings */ + $settings = $className::getRestartSettings(); + + if ($settings === null) { + return $this->loaded ? [self::INVALID] : [self::MISSING]; + } + + $settings['scannedInis'] = true === $settings['scannedInis'] ? 'true' : 'false'; + $settings['scanDir'] = false === $settings['scanDir'] ? 'false' : $settings['scanDir']; + $settings['phprc'] = false === $settings['phprc'] ? 'false' : $settings['phprc']; + $settings['skipped'] = '' === $settings['skipped'] ? "''" : $settings['skipped']; + + $iniCount = count($settings['inis']); + if ($iniCount === 1) { + $settings['inis'] = $settings['inis'][0]; + } else { + $settings['inis'] = sprintf('(%d inis)', $iniCount); + } + + $data = []; + foreach($settings as $key => $value) { + $data[] = sprintf('%s=%s', $key, $value); + } + return $data; + } +} diff --git a/tests/App/Helpers/FunctionalTestCase.php b/tests/App/Helpers/FunctionalTestCase.php new file mode 100644 index 0000000..2f0be40 --- /dev/null +++ b/tests/App/Helpers/FunctionalTestCase.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Composer\XdebugHandler\Tests\App\Helpers; + +use Composer\XdebugHandler\Tests\App\Framework\AppRunner; +use Composer\XdebugHandler\Tests\App\Framework\LogItem; +use Composer\XdebugHandler\Tests\App\Framework\Logs; +use PHPUnit\Framework\TestCase; + +abstract class FunctionalTestCase extends TestCase +{ + /** @var AppRunner */ + protected $runner; + + protected function setUp(): void + { + $scriptDir = dirname(__DIR__); + $this->runner = new AppRunner($scriptDir); + } + + /** + * @param-out string $actual + */ + protected function compareXdebugVersion(string $version, string $op, ?string &$actual): bool + { + $actual = (string) phpversion('xdebug'); + return version_compare($actual, $version, $op); + } + + protected function checkRestart(Logs $logs, ?int $childCount = null): void + { + $main = $logs->getValuesForProcess(1); + $child = $logs->getValuesForProcess(2); + + self::assertCount(5, $main); + + $count = $childCount === null ? 2 : $childCount; + self::assertCount($count, $child); + + if ($childCount === null) { + $item = $logs->getItemFromList($child, 2); + self::assertStringContainsString('The Xdebug extension is not loaded', $item); + } + } + + protected function checkNoRestart(Logs $logs, ?string $extraExpected = null): void + { + $main = $logs->getValuesForProcess(1); + $child = $logs->getValuesForProcess(2); + + self::assertCount(3, $main); + self::assertCount(0, $child); + + $item = $logs->getItemFromList($main, 3); + self::assertStringContainsString('No restart', $item); + + if ($extraExpected !== null) { + self::assertStringContainsString($extraExpected, $item); + } + } +} \ No newline at end of file diff --git a/tests/App/Helpers/RestarterAllow.php b/tests/App/Helpers/RestarterAllow.php new file mode 100644 index 0000000..f67e9ed --- /dev/null +++ b/tests/App/Helpers/RestarterAllow.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Helpers; + +use Composer\Pcre\Preg; +use Composer\XdebugHandler\XdebugHandler; + +/** + * Restarts without xdebug only if not a help command + * + */ + +class RestarterAllow extends XdebugHandler +{ + protected function requiresRestart(bool $default): bool + { + $argv = is_array($_SERVER['argv'] ?? []) ? $_SERVER['argv'] : []; + // @phpstan-ignore-next-line + $matches = Preg::grep('/^-h$|^(?:--)?help$/', $argv); + $required = count($matches) === 0; + + return $default && $required; + } +} diff --git a/tests/App/Helpers/RestarterIni.php b/tests/App/Helpers/RestarterIni.php new file mode 100644 index 0000000..00ca87d --- /dev/null +++ b/tests/App/Helpers/RestarterIni.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Helpers; + +use Composer\XdebugHandler\XdebugHandler; + +/** + * Restarts with phar.readonly=0 if needed, even if xdebug is not loaded + * + */ + +class RestarterIni extends XdebugHandler +{ + /** @var bool */ + private $required; + + protected function requiresRestart(bool $default): bool + { + $this->required = (bool) ini_get('phar.readonly'); + + return $default || $this->required; + + } + + protected function restart(array $command): void + { + if ($this->required) { + $content = file_get_contents((string) $this->tmpIni); + $content .= 'phar.readonly = 0'.PHP_EOL; + file_put_contents((string) $this->tmpIni, $content); + } + + parent::restart($command); + } +} diff --git a/tests/App/Helpers/RestarterMode.php b/tests/App/Helpers/RestarterMode.php new file mode 100644 index 0000000..3241425 --- /dev/null +++ b/tests/App/Helpers/RestarterMode.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\XdebugHandler\Tests\App\Helpers; + +use Composer\Pcre\Preg; +use Composer\XdebugHandler\XdebugHandler; +use Composer\XdebugHandler\Process; + +/** + * Restarts with xdebug loaded in coverage mode, by uncommenting xdebug in the + * temporary ini file and setting the XDEBUG_MODE environment variable. + * + */ + +class RestarterMode extends XdebugHandler +{ + protected function requiresRestart(bool $default): bool + { + if ($default) { + $version = (string) phpversion('xdebug'); + + if (version_compare($version, '3.1', '>=')) { + $modes = xdebug_info('mode'); + return !in_array('coverage', $modes, true); + } + + // See if xdebug.mode is supported in this version + $iniMode = ini_get('xdebug.mode'); + if ($iniMode === false) { + return false; + } + + // Environment value wins but cannot be empty + $envMode = (string) getenv('XDEBUG_MODE'); + if ($envMode !== '') { + $mode = $envMode; + } else { + $mode = $iniMode !== '' ? $iniMode : 'off'; + } + + // An empty comma-separated list is treated as mode 'off' + if (Preg::isMatch('/^,+$/', str_replace(' ', '', $mode))) { + $mode = 'off'; + } + + $modes = explode(',', str_replace(' ', '', $mode)); + return !in_array('coverage', $modes, true); + } + + return false; + } + + protected function restart(array $command): void + { + // uncomment last xdebug line + $regex = '/^;\s*(zend_extension\s*=.*xdebug.*)$/mi'; + $content = (string) file_get_contents((string) $this->tmpIni); + + if (Preg::isMatchAll($regex, $content, $matches)) { + $index = count($matches[1]) -1; + $line = $matches[1][$index]; + $content .= $line.PHP_EOL; + } + + file_put_contents((string) $this->tmpIni, $content); + Process::setEnv('XDEBUG_MODE', 'coverage'); + + parent::restart($command); + } +} diff --git a/tests/App/README.md b/tests/App/README.md new file mode 100644 index 0000000..15a0539 --- /dev/null +++ b/tests/App/README.md @@ -0,0 +1,126 @@ +# Functional Test Scripts + +This folder contains the PHP scripts that produce the output for the functional tests. + +They can be run from the command-line to provide a visual representation of xdebug-handler's inner +workings. + +## Usage + +There are two core scripts, `app-basic` which includes xdebug-handler, and `app-plain` which +does not. + +```sh +cd tests/App + +php app-basic.php --display +php app-plain.php --display +``` + +The `--display` options provides colorized output with a detailed list of xdebug-handler values: + +* [active] - from isXdebugActive() +* [skipped] - from getSkippedVersion() +* [env] - environment variables (PHPRC and PHP_INI_SCAN_DIR) +* [ini] - from getAllIniFiles(). Use the `--inis` options to show them all. +* [settings] - from getRestartSettings() + +These values are obtained after any restart has happened. + +## Scripts + +The following scripts are in addition to the core scripts (which are used by some of these). +### app-extend-allow + +Demonstrates how an extended class can decide whether to restart or not. + +```sh +cd tests/App + +# restarts if no help command or option +php app-extend-allow.php --display + +# no restart with -h, --help or help +php app-extend-allow.php -h --display +``` + +### app-extend-ini + +Demonstrates how an extended class can restart with a specific ini setting, even if xdebug is not +loaded. The `phar_readonly` ini setting is used in this example. + +```sh +cd tests/App + +php -dphar.read_only=1 app-extend-ini.php --display +``` + +### app-extend-mode + +Demonstrates how xdebug can be restarted in a different mode, in this example `xdebug.mode=coverage`. + +```sh +cd tests/App + +php -dxdebug.mode=develop app-extend-mode.php --display +``` + +### app-persistent + +Demonstrates the use of persistent settings (to remove xdebug from all sub-processes) and PhpConfig +to enable xdebug when required. The following sub-processes are called: + + * app-plain.php (xdebug not loaded) + * app-plain.php with original settings (xdebug will be loaded) + * app-other.php (xdebug not loaded) +```sh +php app-persistent.php --display +``` + +### app-prepend + +Demonstrates the use of an auto prepend file to remove xdebug from a script. Note that either +`app-basic` or `app-plain` can be used here. + +```sh +cd tests/App + +php -dauto_prepend_file=app-prepend.php app-plain.php --display +``` + +The `auto_prepend_file` ini setting from the command-line is picked up when the ini content is +merged, so it is available in the restart. + +If stdin is used, it will be read in the restart: + +```sh +# Unixy +cat app-plain.php | php -dauto_prepend_file=app-prepend.php -- --display + +# Windows +type app-plain.php | php -dauto_prepend_file=app-prepend.php -- --display +``` + +### app-stdin + +Included here for demonstration purposes only. + +Uses xdebug-handler from stdin to remove xdebug from a script. Note that either `app-basic` or +`app-plain` can be used here. + +```sh +cd tests/App + +# Unixy +cat app-stdin.php | php -- app-plain.php --display + +# Windows +type app-stdin.php | php -- app-plain.php --display +``` + +While this works in simple scenarios, it is not recommended because the restart settings are not +available. This is due to a different script restarting, thus preventing `app-stdin` from setting +the restart environment variable. + +Additionally _getAllIniFiles()_ returns the temp ini used in the +restart, because the restarting script is not looking for its own named environment variable. diff --git a/tests/App/app-basic.php b/tests/App/app-basic.php new file mode 100644 index 0000000..d8fee4b --- /dev/null +++ b/tests/App/app-basic.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +require __DIR__.'/../../vendor/autoload.php'; + +use Composer\XdebugHandler\Tests\App\Framework\AppHelper; +use Composer\XdebugHandler\XdebugHandler; + +/** + * Restarts without xdebug + * + * Usage: php app.php + */ + +$app = new AppHelper(__FILE__); + +$app->write('start'); + +$xdebug = $app->getXdebugHandler(); +$xdebug->check(); + +$app->writeXdebugStatus(); +$app->write('finish'); diff --git a/tests/App/app-extend-allow.php b/tests/App/app-extend-allow.php new file mode 100644 index 0000000..425e83e --- /dev/null +++ b/tests/App/app-extend-allow.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +require __DIR__.'/../../vendor/autoload.php'; + +use Composer\XdebugHandler\Tests\App\Helpers\RestarterAllow; +use Composer\XdebugHandler\Tests\App\Framework\AppHelper; +use Composer\XdebugHandler\XdebugHandler; + +/** + * No restart if a help command (-h, --help, help) + * + * Usage: php app-extend-help.php -h + * + */ + +$app = new AppHelper(__FILE__); + +$app->write('start'); + +$xdebug = $app->getXdebugHandler(RestarterAllow::class); +$xdebug->check(); + +$app->writeXdebugStatus(); +$app->write('finish'); diff --git a/tests/App/app-extend-ini.php b/tests/App/app-extend-ini.php new file mode 100644 index 0000000..ffdac27 --- /dev/null +++ b/tests/App/app-extend-ini.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +require __DIR__.'/../../vendor/autoload.php'; + +use Composer\XdebugHandler\Tests\App\Helpers\RestarterIni; +use Composer\XdebugHandler\Tests\App\Framework\AppHelper; +use Composer\XdebugHandler\XdebugHandler; + +/** + * Restarts without xdebug with phar.read_only=0, even if xdebug is not loaded + * + * Usage: php -dphar.read_only=1 app-extend-ini.php + */ + +$app = new AppHelper(__FILE__); + +$app->write('start'); + +$setting = (int) ini_get('phar.readonly'); +$app->write('initial phar.readonly='.$setting); + +$xdebug = $app->getXdebugHandler(RestarterIni::class); +$xdebug->check(); + +$app->writeXdebugStatus(); +$app->write('finish'); diff --git a/tests/App/app-extend-mode.php b/tests/App/app-extend-mode.php new file mode 100644 index 0000000..4769356 --- /dev/null +++ b/tests/App/app-extend-mode.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +require __DIR__.'/../../vendor/autoload.php'; + +use Composer\XdebugHandler\Tests\App\Helpers\RestarterMode; +use Composer\XdebugHandler\Tests\App\Framework\AppHelper; +use Composer\XdebugHandler\XdebugHandler; + +/** + * Restarts with xdebug and xdebug.mode=coverage + * + * Usage: php -dxdebug.mode=develop app-extend-mode.php + */ + +$app = new AppHelper(__FILE__); + +$app->write('start'); + +$xdebug = $app->getXdebugHandler(RestarterMode::class); +$xdebug->check(); + +$app->writeXdebugStatus(); +$app->write('finish'); diff --git a/tests/App/app-persistent.php b/tests/App/app-persistent.php new file mode 100644 index 0000000..cb1a8d3 --- /dev/null +++ b/tests/App/app-persistent.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +require __DIR__.'/../../vendor/autoload.php'; + +use Composer\XdebugHandler\Tests\App\Framework\AppHelper; +use Composer\XdebugHandler\XdebugHandler; +use Composer\XdebugHandler\PhpConfig; + +/** + * Restarts without xdebug in persistent mode, then calls sub-processes: + * - app-plain.php (xdebug not loaded) + * - app-plain.php with original settings (xdebug will be loaded) + * - app-other.php (xdebug not loaded) + * + * Usage: php app-persistent.php + */ + +$app = new AppHelper(__FILE__); + +$app->write('start'); + +$xdebug = $app->getXdebugHandler(null, ['persistent' => '']); +$xdebug->check(); + +$app->writeXdebugStatus(); + +// Simple call to app-plain (xdebug should not be loaded) +$app->write('CALL app-plain.php'); +$app->write('- if we are in a restart, xdebug should not be loaded'); +$app->runScript('app-plain.php'); + +// app-plain with xdebug +$app->write('CALL app-plain.php with original settings'); +$app->write('- if we are in a restart, xdebug will be loaded'); + +$phpConfig = new PhpConfig; +$phpConfig->useOriginal(); +$app->runScript('app-plain.php'); + +// restore persistent settings +$phpConfig->usePersistent(); + +// Simple call to app-basic (xdebug should not be loaded) +$app->write('CALL app-basic.php'); +$app->write('- if we are in a restart, xdebug should not be loaded'); +$app->runScript('app-basic.php'); + +$app->write('finish'); diff --git a/tests/App/app-plain.php b/tests/App/app-plain.php new file mode 100644 index 0000000..664e974 --- /dev/null +++ b/tests/App/app-plain.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +require __DIR__.'/../../vendor/autoload.php'; + +use Composer\XdebugHandler\Tests\App\Framework\AppHelper; + +/** + * A plain php script that does not include xdebug-handler + * + * Usage: php app-none.php + */ + +$app = new AppHelper(__FILE__); + +$app->write('start'); +$app->writeXdebugStatus(); +$app->write('finish'); diff --git a/tests/App/app-prepend.php b/tests/App/app-prepend.php new file mode 100644 index 0000000..fa6f172 --- /dev/null +++ b/tests/App/app-prepend.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +require __DIR__.'/../../vendor/autoload.php'; + +use Composer\XdebugHandler\Tests\App\Framework\AppHelper; +use Composer\XdebugHandler\XdebugHandler; + +/** + * Restarts without xdebug using an autoprepend file + * + * Usage: php -dauto_prepend_file=app-prepend.php app-plain.php + * + * The auto_prepend_file ini setting from the command-line is picked up when the + * ini content is merged, so it is available in the restart. + * + * If stdin is used, it will be read in the restart: + * Usage (Unixy) : cat app-plain.php | php -dauto_prepend_file=app-prepend.php + * Usage (Windows) : type app-plain.php | php -dauto_prepend_file=app-prepend.php + */ + +$app = new AppHelper(__FILE__); + +$app->write('start'); + +$mainScript = $app->getServerArgv0(); +$app->write('initial argv0 '.$mainScript); + +// Set mainScript. Has no effect in restarted process +$settings = null; +if ($mainScript === 'Standard input code') { + $settings = ['mainScript' => '--']; +} + +$xdebug = $app->getXdebugHandler(null, $settings); +$xdebug->check(); + +$app->writeXdebugStatus(); +$app->write('finish'); diff --git a/tests/App/app-stdin.php b/tests/App/app-stdin.php new file mode 100644 index 0000000..716ef6b --- /dev/null +++ b/tests/App/app-stdin.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +require __DIR__.'/../../vendor/autoload.php'; + +use Composer\XdebugHandler\Tests\App\Framework\AppHelper; +use Composer\XdebugHandler\XdebugHandler; + +/** + * Restarts without xdebug using stdin + * + * Usage (Unixy) : cat app-stdin.php | php -- app-plain.php + * Usage (Windows) : type app-stdin.php | php -- app-plain.php + * + */ + +$app = new AppHelper(__FILE__); + +$app->write('start'); + +$argv = $app->getServerArgv(); +$mainScript = $argv[0]; +$app->write('working: argv0 '. $mainScript); + +// Set mainScript. Has no effect in restarted process +$settings = null; +if ($mainScript === 'Standard input code' && isset($argv[1])) { + $settings = ['mainScript' => $argv[1]]; + // @phpstan-ignore-next-line + unset($_SERVER['argv'][1]); +} + +$xdebug = $app->getXdebugHandler(null, $settings); +$xdebug->check(); + +$app->writeXdebugStatus(); +$app->write('finish'); diff --git a/tests/AppNoRestartTest.php b/tests/AppNoRestartTest.php new file mode 100644 index 0000000..ab4c757 --- /dev/null +++ b/tests/AppNoRestartTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Composer\XdebugHandler\Tests; + +use Composer\XdebugHandler\Tests\App\Framework\AppRunner; +use Composer\XdebugHandler\Tests\App\Framework\PhpOptions; +use Composer\XdebugHandler\Tests\App\Helpers\FunctionalTestCase; +use PHPUnit\Framework\TestCase; + +/** + * @group functional + */ +class AppNoRestartTest extends FunctionalTestCase +{ + /** + * Tests no restart when env allow is truthy + * + * @requires extension xdebug + */ + public function testNoRestartWhenAllowed(): void + { + $script = 'app-basic.php'; + + $logs = $this->runner->run($script, null, true); + $this->checkNoRestart($logs); + + $appName = $this->runner->getAppName($script); + $child = $logs->getValuesForProcessName(1, $appName); + $item = $logs->getItemFromList($child, 2); + self::assertStringContainsString('The Xdebug extension is loaded', $item); + } + + /** + * Tests no restart when xdebug.mode is off + * + * @requires extension xdebug + */ + public function testNoRestartWhenModeOff(): void + { + if ($this->compareXdebugVersion('3.1', '<', $version)) { + self::markTestSkipped('Not supported in xdebug version '.$version); + } + + $script = 'app-basic.php'; + + $options = new PhpOptions(); + $options->addPhpArgs('-dxdebug.mode=off'); + + $logs = $this->runner->run($script, $options); + $this->checkNoRestart($logs, 'Allowed by xdebug.mode'); + } + + /** + * Tests no restart when proc_open is disabled + * + * @requires extension xdebug + */ + function testNoRestartWhenConfigError(): void + { + $script = 'app-basic.php'; + + $options = new PhpOptions(); + $options->addPhpArgs('-ddisable_functions=proc_open'); + + $logs = $this->runner->run($script, $options); + $this->checkNoRestart($logs, 'proc_open function is disabled'); + } + + /** + * Tests no restart when allowed by an application + * + * @requires extension xdebug + */ + function testNoRestartWhenAllowedByApplication(): void + { + $script = 'app-extend-allow.php'; + + $options = new PhpOptions(); + $options->addScriptArgs('--help'); + + $logs = $this->runner->run($script, $options); + $this->checkNoRestart($logs, 'Allowed by application'); + } +} diff --git a/tests/AppRestartTest.php b/tests/AppRestartTest.php new file mode 100644 index 0000000..ab47f57 --- /dev/null +++ b/tests/AppRestartTest.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Composer\XdebugHandler\Tests; + +use Composer\XdebugHandler\Tests\App\Framework\AppRunner; +use Composer\XdebugHandler\Tests\App\Framework\LogItem; +use Composer\XdebugHandler\Tests\App\Framework\PhpOptions; +use Composer\XdebugHandler\Tests\App\Helpers\FunctionalTestCase; +use PHPUnit\Framework\TestCase; + +/** + * @group functional + */ +class AppRestartTest extends FunctionalTestCase +{ + /** + * + * @requires extension xdebug + */ + public function testRestart(): void + { + $script = 'app-basic.php'; + + $logs = $this->runner->run($script); + $this->checkRestart($logs); + } + + /** + * + * @requires extension xdebug + */ + public function testRestartWithPrependFile(): void + { + $script = 'app-plain.php'; + + $options = new PhpOptions(); + $options->addPhpArgs('-dauto_prepend_file=app-prepend.php'); + + $logs = $this->runner->run($script, $options); + $this->checkRestart($logs); + + // check app-plain has run + $appName = $this->runner->getAppName($script); + $child = $logs->getValuesForProcessName(2, $appName); + self::assertNotEmpty($child); + } + + /** + * + * @requires extension xdebug + */ + public function testRestartWithPrependFileStdin(): void + { + $script = 'app-plain.php'; + + $options = new PhpOptions(); + $options->addPhpArgs('-dauto_prepend_file=app-prepend.php'); + $options->setStdin(true); + + $logs = $this->runner->run($script, $options); + $this->checkRestart($logs); + + // check app-plain has run + $appName = 'Standard input code'; + $child = $logs->getValuesForProcessName(2, $appName); + self::assertNotEmpty($child); + } + + /** + * + * @requires extension xdebug + */ + public function testRestartWithStdin(): void + { + $script = 'app-stdin.php'; + + $options = new PhpOptions(); + $options->addPhpArgs('--'); + $options->addScriptArgs('app-plain.php'); + $options->setStdin(true); + + $logs = $this->runner->run($script, $options); + $this->checkRestart($logs, 0); + + // check not loaded from app-plain + $appName = $this->runner->getAppName('app-plain.php'); + $child = $logs->getValuesForProcessName(2, $appName); + $item = $logs->getItemFromList($child, 2); + self::assertStringContainsString('The Xdebug extension is not loaded', $item); + } + + /** + * + * @requires extension xdebug + */ + public function testRestartWithIniChange(): void + { + $script = 'app-extend-ini.php'; + + $options = new PhpOptions(); + $options->addPhpArgs('-dphar.read_only=1'); + + $logs = $this->runner->run($script, $options); + $this->checkRestart($logs); + + // check ini setting has changed in child + $appName = $this->runner->getAppName($script); + $child = $logs->getValuesForProcessName(2, $appName); + $item = $logs->getItemFromList($child, 2); + self::assertStringContainsString('phar.readonly=0', $item); + } + + /** + * + * @requires extension xdebug + */ + public function testRestartWithXdebug(): void + { + $version = (string) phpversion('xdebug'); + + if (version_compare($version, '3.0', '<')) { + self::markTestSkipped('Not supported in xdebug version '.$version); + } + + $script = 'app-extend-mode.php'; + + $options = new PhpOptions(); + $options->addPhpArgs('-dxdebug.mode=develop'); + + $logs = $this->runner->run($script, $options); + $this->checkRestart($logs, 2); + + $child = $logs->getValuesForProcess(2); + $item = $logs->getItemFromList($child, 2); + self::assertStringContainsString('The Xdebug extension is loaded', $item); + self::assertStringContainsString('xdebug.mode=coverage', $item); + } + + /** + * + * @requires extension xdebug + */ + public function testRestartWithProcessPersistent(): void + { + $script = 'app-persistent.php'; + + $logs = $this->runner->run($script); + $this->checkRestart($logs); + + // first sub-process - check not loaded from app-plain + $appName = $this->runner->getAppName('app-plain.php'); + $child = $logs->getValuesForProcessName(3, $appName); + $item = $logs->getItemFromList($child, 2); + self::assertStringContainsString('The Xdebug extension is not loaded', $item); + + // second sub-process - check loaded from app-plain + $appName = $this->runner->getAppName('app-plain.php'); + $child = $logs->getValuesForProcessName(4, $appName); + $item = $logs->getItemFromList($child, 2); + self::assertStringContainsString('The Xdebug extension is loaded', $item); + + // third sub-process - check not loaded from app-basic + $child = $logs->getValuesForProcess(5); + $item = $logs->getItemFromList($child, 2); + self::assertStringContainsString('The Xdebug extension is not loaded', $item); + + $item = $logs->getItemFromList($child, 3); + self::assertStringContainsString('Process called with existing restart settings', $item); + } +}