151 lines
4.1 KiB
PHP
151 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Support\Api;
|
|
|
|
use RuntimeException;
|
|
|
|
final class ApiServer
|
|
{
|
|
private mixed $process = null;
|
|
private string $stdoutLog;
|
|
private string $stderrLog;
|
|
|
|
public function __construct(
|
|
private readonly string $workingDirectory,
|
|
private readonly string $host,
|
|
private readonly int $port,
|
|
) {
|
|
$logDirectory = app_path('build/logs');
|
|
if (!is_dir($logDirectory)) {
|
|
mkdir($logDirectory, 0777, true);
|
|
}
|
|
|
|
$this->stdoutLog = $logDirectory . DIRECTORY_SEPARATOR . 'api-server.out.log';
|
|
$this->stderrLog = $logDirectory . DIRECTORY_SEPARATOR . 'api-server.err.log';
|
|
}
|
|
|
|
public function start(): void
|
|
{
|
|
$command = sprintf(
|
|
'%s -S %s index.php',
|
|
escapeshellarg((string)(PHP_BINARY ?: 'php')),
|
|
escapeshellarg($this->host . ':' . $this->port),
|
|
);
|
|
$environment = $this->inheritEnvironment();
|
|
|
|
$descriptorSpec = [
|
|
0 => ['pipe', 'r'],
|
|
1 => ['file', $this->stdoutLog, 'a'],
|
|
2 => ['file', $this->stderrLog, 'a'],
|
|
];
|
|
|
|
$this->process = proc_open($command, $descriptorSpec, $pipes, $this->workingDirectory, $environment);
|
|
if (!is_resource($this->process)) {
|
|
throw new RuntimeException('Unable to start the PHP test server.');
|
|
}
|
|
|
|
if (isset($pipes[0]) && is_resource($pipes[0])) {
|
|
fclose($pipes[0]);
|
|
}
|
|
}
|
|
|
|
public function isRunning(): bool
|
|
{
|
|
if (!is_resource($this->process)) {
|
|
return false;
|
|
}
|
|
|
|
$status = proc_get_status($this->process);
|
|
|
|
return (bool)($status['running'] ?? false);
|
|
}
|
|
|
|
public function stop(): void
|
|
{
|
|
if (!is_resource($this->process)) {
|
|
return;
|
|
}
|
|
|
|
proc_terminate($this->process);
|
|
usleep(250000);
|
|
|
|
$status = proc_get_status($this->process);
|
|
if (($status['running'] ?? false) && function_exists('posix_kill')) {
|
|
@posix_kill((int)$status['pid'], 9);
|
|
}
|
|
|
|
proc_close($this->process);
|
|
$this->process = null;
|
|
}
|
|
|
|
public function describeFailure(string $message): string
|
|
{
|
|
$stderr = is_file($this->stderrLog) ? trim((string)file_get_contents($this->stderrLog)) : '';
|
|
if ($stderr !== '') {
|
|
return $message . PHP_EOL . $stderr;
|
|
}
|
|
|
|
return $message;
|
|
}
|
|
|
|
public static function findAvailablePort(string $host): int
|
|
{
|
|
$socket = @stream_socket_server('tcp://' . $host . ':0', $errorCode, $errorMessage);
|
|
if ($socket === false) {
|
|
throw new RuntimeException(
|
|
sprintf('Unable to reserve an API test port on %s: %s (%d)', $host, $errorMessage, $errorCode)
|
|
);
|
|
}
|
|
|
|
$address = stream_socket_get_name($socket, false);
|
|
fclose($socket);
|
|
|
|
if (!is_string($address) || $address === '') {
|
|
throw new RuntimeException('Unable to determine the reserved API test port.');
|
|
}
|
|
|
|
$lastSeparator = strrpos($address, ':');
|
|
if ($lastSeparator === false) {
|
|
throw new RuntimeException('Unable to parse the reserved API test port.');
|
|
}
|
|
|
|
$port = (int)substr($address, $lastSeparator + 1);
|
|
if ($port <= 0) {
|
|
throw new RuntimeException('Unable to parse a valid API test port.');
|
|
}
|
|
|
|
return $port;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function inheritEnvironment(): array
|
|
{
|
|
$environment = [];
|
|
|
|
$sources = [getenv(), $_ENV ?? [], $_SERVER ?? []];
|
|
foreach ($sources as $source) {
|
|
if (!is_array($source)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($source as $key => $value) {
|
|
if (!is_string($key) || $key === '') {
|
|
continue;
|
|
}
|
|
|
|
if (!is_scalar($value) && $value !== null) {
|
|
continue;
|
|
}
|
|
|
|
$environment[$key] = $value === null ? '' : (string)$value;
|
|
}
|
|
}
|
|
|
|
return $environment;
|
|
}
|
|
}
|