diff --git a/README.md b/README.md index bd41f7c..63a6de3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Optionally, you can install Phiremock Server in case you want to have it between "mcustiel/phiremock-codeception-extension": "^2.0", "mcustiel/phiremock-server": "^1.0", "guzzlehttp/guzzle": "^6.0" +} ``` Phiremock server has been made an optional dependency in case you want to run it from a phar file, a global composer dependency or in a docker container, and not have it as a project dependency. @@ -39,7 +40,8 @@ extensions: bin_path: ../vendor/bin # defaults to codeception_dir/../vendor/bin logs_path: /var/log/my_app/tests/logs # defaults to codeception's tests output dir debug: true # defaults to false - start_delay: 1 # default to 0 + wait_until_ready: true # defaults to false + wait_until_ready_timeout: 15 # defaults to 30 expectations_path: /my/expectations/path # defaults to tests/_expectations server_factory: \My\FactoryClass # defaults to 'default' extra_instances: [] # deaults to an empty array @@ -77,6 +79,17 @@ Time to wait after Phiremock Server is started before running the tests (used to **Default:** 0 +#### wait_until_ready +This is more robust alternative to start_delay. It will check if Phiremock Server is actually running before running the tests. +Note: it depends on Phiremeock Client to be installed via composer (it is used to check the status of Phiremock Server). + +**Default:** false + +#### wait_until_ready_timeout +This will be used only if wait_until_ready is set to true. You can specify after how many seconds it will stop checking if Phiremock Server is running. + +**Default:** 30 + #### expectations_path Specifies a directory to search for json files defining expectations to load by default. @@ -116,7 +129,6 @@ extensions: \Codeception\Extension\Phiremock: listen: 127.0.0.1:18080 debug: true - start_delay: 1 expectations_path: /my/expectations/path-1 suites: - acceptance diff --git a/src/Extension/Phiremock.php b/src/Extension/Phiremock.php index 7e59575..3657ecc 100644 --- a/src/Extension/Phiremock.php +++ b/src/Extension/Phiremock.php @@ -24,6 +24,7 @@ use Codeception\Suite; use Mcustiel\Phiremock\Codeception\Extension\Config; use Mcustiel\Phiremock\Codeception\Extension\PhiremockProcessManager; +use Mcustiel\Phiremock\Codeception\Extension\ReadinessCheckerFactory; class Phiremock extends CodeceptionExtension { @@ -68,6 +69,7 @@ public function startProcess(SuiteEvent $event): void } } $this->executeDelay(); + $this->waitUntilReady(); } public function stopProcess(): void @@ -108,4 +110,38 @@ private function setDefaultLogsPath(): void $this->config['logs_path'] = Config::getDefaultLogsPath(); } } + + private function waitUntilReady(): void + { + if (!$this->extensionConfig->isWaitUntilReady()) { + return; + } + + $this->writeln('Waiting until Phiremock is ready...'); + + $readinessChecker = ReadinessCheckerFactory::create( + $this->extensionConfig->getInterface(), + $this->extensionConfig->getPort(), + $this->extensionConfig->isSecure() + ); + + $start = \microtime(true); + + while (true) { + if ($readinessChecker->isReady()) { + break; + } + + \sleep(1); + $elapsed = (int) (\microtime(true) - $start); + + if ($elapsed > $this->extensionConfig->getWaitUntilReadyTimeout()) { + throw new \RuntimeException( + \sprintf('Phiremock failed to start within %d seconds', $this->extensionConfig->getWaitUntilReadyTimeout()) + ); + } + } + + $this->writeln('Phiremock is ready!'); + } } diff --git a/src/PhiremockExtension/Config.php b/src/PhiremockExtension/Config.php index 2042995..41665e4 100644 --- a/src/PhiremockExtension/Config.php +++ b/src/PhiremockExtension/Config.php @@ -36,19 +36,23 @@ class Config public const DEFAULT_SERVER_FACTORY = 'default'; public const DEFAULT_EXTRA_INSTANCES = []; public const DEFAULT_SUITES = []; + public const DEFAULT_WAIT_UNTIL_READY = false; + public const DEFAULT_WAIT_UNTIL_READY_TIMEOUT = 30; public const DEFAULT_CONFIG = [ - 'listen' => self::DEFAULT_INTERFACE . ':' . self::DEFAULT_PORT, - 'debug' => self::DEFAULT_DEBUG_MODE, - 'start_delay' => self::DEFAULT_DELAY, - 'bin_path' => self::DEFAULT_PHIREMOCK_PATH, - 'expectations_path' => self::DEFAULT_EXPECTATIONS_PATH, - 'server_factory' => self::DEFAULT_SERVER_FACTORY, - 'certificate' => self::DEFAULT_CERTIFICATE, - 'certificate_key' => self::DEFAULT_CERTIFICATE_KEY, - 'cert_passphrase' => self::DEFAULT_CERTIFICATE_PASSPHRASE, - 'extra_instances' => self::DEFAULT_EXTRA_INSTANCES, - 'suites' => self::DEFAULT_SUITES, + 'listen' => self::DEFAULT_INTERFACE . ':' . self::DEFAULT_PORT, + 'debug' => self::DEFAULT_DEBUG_MODE, + 'start_delay' => self::DEFAULT_DELAY, + 'bin_path' => self::DEFAULT_PHIREMOCK_PATH, + 'expectations_path' => self::DEFAULT_EXPECTATIONS_PATH, + 'server_factory' => self::DEFAULT_SERVER_FACTORY, + 'certificate' => self::DEFAULT_CERTIFICATE, + 'certificate_key' => self::DEFAULT_CERTIFICATE_KEY, + 'cert_passphrase' => self::DEFAULT_CERTIFICATE_PASSPHRASE, + 'extra_instances' => self::DEFAULT_EXTRA_INSTANCES, + 'suites' => self::DEFAULT_SUITES, + 'wait_until_ready' => self::DEFAULT_WAIT_UNTIL_READY, + 'wait_until_ready_timeout' => self::DEFAULT_WAIT_UNTIL_READY_TIMEOUT ]; /** @var string */ @@ -79,6 +83,10 @@ class Config private $suites; /** @var callable */ private $output; + /** @var bool */ + private $waitUntilReady; + /** @var int */ + private $waitUntilReadyTimeout; /** @throws ConfigurationException */ public function __construct(array $config, callable $output) @@ -96,6 +104,8 @@ public function __construct(array $config, callable $output) $this->certificatePassphrase = $config['cert_passphrase']; $this->initExtraInstances($config); $this->suites = $config['suites']; + $this->waitUntilReady = (bool) $config['wait_until_ready']; + $this->waitUntilReadyTimeout = (int) $config['wait_until_ready_timeout']; } public function getSuites(): array @@ -168,6 +178,22 @@ public function getExtraInstances(): array return $this->extraInstances; } + public function isSecure(): bool + { + return $this->getCertificatePath() !== null + && $this->getCertificateKeyPath() !== null; + } + + public function isWaitUntilReady(): bool + { + return $this->waitUntilReady; + } + + public function getWaitUntilReadyTimeout(): int + { + return $this->waitUntilReadyTimeout; + } + /** @throws ConfigurationException */ public static function getDefaultLogsPath(): string { diff --git a/src/PhiremockExtension/ReadinessChecker/CurlChecker.php b/src/PhiremockExtension/ReadinessChecker/CurlChecker.php new file mode 100644 index 0000000..3eb1709 --- /dev/null +++ b/src/PhiremockExtension/ReadinessChecker/CurlChecker.php @@ -0,0 +1,49 @@ +. + */ + +namespace Mcustiel\Phiremock\Codeception\Extension\ReadinessChecker; + +use Mcustiel\Phiremock\Codeception\Extension\ReadinessCheckerInterface; + +class CurlChecker implements ReadinessCheckerInterface +{ + private $url; + + public function __construct(string $url) + { + $this->url = rtrim($url, '/'); + } + + public function isReady(): bool + { + $ch = \curl_init(); + + \curl_setopt($ch, CURLOPT_URL,$this->url . '/__phiremock/reset'); + \curl_setopt($ch, CURLOPT_POST, 1); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $output = \curl_exec($ch); + \curl_close($ch); + + if ($output === false) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/PhiremockExtension/ReadinessChecker/PhiremockClientChecker.php b/src/PhiremockExtension/ReadinessChecker/PhiremockClientChecker.php new file mode 100644 index 0000000..7766916 --- /dev/null +++ b/src/PhiremockExtension/ReadinessChecker/PhiremockClientChecker.php @@ -0,0 +1,44 @@ +. + */ + +namespace Mcustiel\Phiremock\Codeception\Extension\ReadinessChecker; + +use GuzzleHttp\Exception\ConnectException; +use Mcustiel\Phiremock\Codeception\Extension\ReadinessCheckerInterface; +use Mcustiel\Phiremock\Client\Phiremock; +use Psr\Http\Client\ClientExceptionInterface; + +class PhiremockClientChecker implements ReadinessCheckerInterface +{ + private $client; + + public function __construct(Phiremock $client) + { + $this->client = $client; + } + + public function isReady(): bool + { + try { + $this->client->reset(); + return true; + } catch (ConnectException $e) {} + + return false; + } +} \ No newline at end of file diff --git a/src/PhiremockExtension/ReadinessCheckerFactory.php b/src/PhiremockExtension/ReadinessCheckerFactory.php new file mode 100644 index 0000000..fcbe8f9 --- /dev/null +++ b/src/PhiremockExtension/ReadinessCheckerFactory.php @@ -0,0 +1,55 @@ +. + */ + +namespace Mcustiel\Phiremock\Codeception\Extension; + +use Mcustiel\Phiremock\Client\Factory; +use Mcustiel\Phiremock\Client\Connection\Host; +use Mcustiel\Phiremock\Client\Connection\Port; +use Mcustiel\Phiremock\Client\Connection\Scheme; +use Mcustiel\Phiremock\Codeception\Extension\ReadinessChecker\CurlChecker; +use Mcustiel\Phiremock\Codeception\Extension\ReadinessChecker\PhiremockClientChecker; + +class ReadinessCheckerFactory +{ + public static function create(string $host, string $port, bool $isSecure): ReadinessCheckerInterface + { + if (class_exists(Factory::class)) { + $phiremockClient = Factory::createDefault() + ->createPhiremockClient( + new Host($host), + new Port($port), + $isSecure ? Scheme::createHttps() : Scheme::createHttp() + ); + + return new PhiremockClientChecker( + $phiremockClient + ); + } elseif (extension_loaded('curl')) { + $url = 'http' . ($isSecure ? 's' : '') + . '://' . $host + . ($port !== '' ? ':' . $port : ''); + + return new CurlChecker($url); + } + + throw new \RuntimeException( + 'Config wait_until_ready is enabled but no readiness checker can be run. Check if you have Phiremock Client installed or curl extension enabled.' + ); + } +} \ No newline at end of file diff --git a/src/PhiremockExtension/ReadinessCheckerInterface.php b/src/PhiremockExtension/ReadinessCheckerInterface.php new file mode 100644 index 0000000..08c6270 --- /dev/null +++ b/src/PhiremockExtension/ReadinessCheckerInterface.php @@ -0,0 +1,24 @@ +. + */ + +namespace Mcustiel\Phiremock\Codeception\Extension; + +interface ReadinessCheckerInterface +{ + public function isReady(): bool; +}