mirror of
https://github.com/stan220/testcontainers-php.git
synced 2026-09-12 11:29:36 +00:00
Merge pull request #19 from rw4lll/feat/docker-engine-api-client
Implement WaitForHttp and WaitForHostPort strategies
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainers\Container;
|
||||||
|
|
||||||
|
enum HttpMethod: string
|
||||||
|
{
|
||||||
|
case GET = 'GET';
|
||||||
|
case POST = 'POST';
|
||||||
|
case PUT = 'PUT';
|
||||||
|
case DELETE = 'DELETE';
|
||||||
|
case HEAD = 'HEAD';
|
||||||
|
case OPTIONS = 'OPTIONS';
|
||||||
|
|
||||||
|
public static function fromString(string $method): self
|
||||||
|
{
|
||||||
|
return self::tryFrom(strtoupper($method)) ?? throw new \InvalidArgumentException("Invalid HTTP method: $method");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,16 @@ abstract class BaseWaitStrategy implements WaitStrategy
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract public function wait(StartedTestContainer $container): void;
|
abstract public function wait(StartedTestContainer $container): void;
|
||||||
|
|
||||||
|
public function withTimeout(int $timeout): static
|
||||||
|
{
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPollInterval(int $pollInterval): static
|
||||||
|
{
|
||||||
|
$this->pollInterval = $pollInterval;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainers\Wait;
|
||||||
|
|
||||||
|
use Testcontainers\Container\StartedTestContainer;
|
||||||
|
use Testcontainers\Exception\ContainerWaitingTimeoutException;
|
||||||
|
|
||||||
|
class WaitForHostPort extends BaseWaitStrategy
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected int $port,
|
||||||
|
int $timeout = 10000,
|
||||||
|
int $pollInterval = 500
|
||||||
|
) {
|
||||||
|
parent::__construct($timeout, $pollInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wait(StartedTestContainer $container): void
|
||||||
|
{
|
||||||
|
$startTime = microtime(true) * 1000;
|
||||||
|
$containerAddress = $container->getHost();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$elapsedTime = (microtime(true) * 1000) - $startTime;
|
||||||
|
|
||||||
|
if ($elapsedTime > $this->timeout) {
|
||||||
|
throw new ContainerWaitingTimeoutException($container->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isPortOpen($containerAddress, $this->port)) {
|
||||||
|
return; // Port is open, container is ready
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep($this->pollInterval * 1000); // Wait for the next polling interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isPortOpen(string $ipAddress, int $port): bool
|
||||||
|
{
|
||||||
|
$connection = @fsockopen($ipAddress, $port, $errno, $errstr, 2);
|
||||||
|
|
||||||
|
if ($connection !== false) {
|
||||||
|
fclose($connection);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
-46
@@ -4,83 +4,149 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Testcontainers\Wait;
|
namespace Testcontainers\Wait;
|
||||||
|
|
||||||
use Docker\Docker;
|
use Testcontainers\Container\HttpMethod;
|
||||||
use Testcontainers\Exception\ContainerNotReadyException;
|
use Testcontainers\Container\StartedTestContainer;
|
||||||
|
use Testcontainers\Exception\ContainerWaitingTimeoutException;
|
||||||
|
|
||||||
//TODO: not ready yet
|
class WaitForHttp extends BaseWaitStrategy
|
||||||
class WaitForHttp implements WaitStrategy
|
|
||||||
{
|
{
|
||||||
public const METHOD_GET = 'GET';
|
protected HttpMethod $method = HttpMethod::GET;
|
||||||
public const METHOD_POST = 'POST';
|
|
||||||
public const METHOD_PUT = 'PUT';
|
|
||||||
public const METHOD_DELETE = 'DELETE';
|
|
||||||
public const METHOD_HEAD = 'HEAD';
|
|
||||||
public const METHOD_OPTIONS = 'OPTIONS';
|
|
||||||
|
|
||||||
|
protected string $path = '/';
|
||||||
|
|
||||||
private string $method = 'GET';
|
protected string $protocol = 'http';
|
||||||
private string $path = '/';
|
|
||||||
private int $statusCode = 200;
|
|
||||||
private Docker $dockerClient;
|
|
||||||
|
|
||||||
public function __construct(private int $port)
|
protected int $expectedStatusCode = 200;
|
||||||
{
|
|
||||||
$this->dockerClient = Docker::create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function make(int $port): self
|
protected bool $allowInsecure = false;
|
||||||
{
|
|
||||||
return new WaitForHttp($port);
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
protected array $headers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int Timeout in milliseconds for reading the response
|
||||||
|
*/
|
||||||
|
protected int $readTimeout = 1000;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected int $port,
|
||||||
|
int $timeout = 10000,
|
||||||
|
int $pollInterval = 500
|
||||||
|
) {
|
||||||
|
parent::__construct($timeout, $pollInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param WaitForHttp::METHOD_* $method
|
* @deprecated Use constructor instead
|
||||||
|
* Kept for backward compatibility
|
||||||
|
* Should be removed in next major version
|
||||||
*/
|
*/
|
||||||
public function withMethod(string $method): self
|
public static function make(int $port): self
|
||||||
{
|
{
|
||||||
$this->method = $method;
|
return new self($port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param HttpMethod|value-of<HttpMethod> $method
|
||||||
|
*/
|
||||||
|
public function withMethod(HttpMethod | string $method): self
|
||||||
|
{
|
||||||
|
if (is_string($method)) {
|
||||||
|
$method = HttpMethod::fromString($method);
|
||||||
|
}
|
||||||
|
$this->method = $method;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withPath(string $path): self
|
public function withPath(string $path): self
|
||||||
{
|
{
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withStatusCode(int $statusCode): self
|
public function withExpectedStatusCode(int $statusCode): self
|
||||||
{
|
{
|
||||||
$this->statusCode = $statusCode;
|
$this->expectedStatusCode = $statusCode;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function wait(string $id): void
|
public function usingHttps(): self
|
||||||
{
|
{
|
||||||
$containerNetworks = $this->dockerClient->containerInspect($id)->getNetworkSettings()->getNetworks();
|
$this->protocol = 'https';
|
||||||
$containerAddress = null;
|
return $this;
|
||||||
foreach ($containerNetworks as $network) {
|
}
|
||||||
if ($network->getNetworkID() === $id) {
|
|
||||||
$containerAddress = $network->getIpAddress();
|
public function allowInsecure(): self
|
||||||
break;
|
{
|
||||||
|
$this->allowInsecure = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withReadTimeout(int $timeout): self
|
||||||
|
{
|
||||||
|
$this->readTimeout = $timeout;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $headers
|
||||||
|
*/
|
||||||
|
public function withHeaders(array $headers): self
|
||||||
|
{
|
||||||
|
$this->headers = $headers;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wait(StartedTestContainer $container): void
|
||||||
|
{
|
||||||
|
$startTime = microtime(true) * 1000;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$elapsedTime = (microtime(true) * 1000) - $startTime;
|
||||||
|
|
||||||
|
if ($elapsedTime > $this->timeout) {
|
||||||
|
throw new ContainerWaitingTimeoutException($container->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$containerAddress = $container->getHost();
|
||||||
|
|
||||||
|
$url = sprintf('%s://%s:%d%s', $this->protocol, $containerAddress, $this->port, $this->path);
|
||||||
|
$responseCode = $this->makeHttpRequest($url);
|
||||||
|
|
||||||
|
if ($responseCode === $this->expectedStatusCode) {
|
||||||
|
return; // Container is ready
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep($this->pollInterval * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeHttpRequest(string $url): int
|
||||||
|
{
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method->value);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_NOBODY, true); // No need for response body, just headers
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->readTimeout);
|
||||||
|
|
||||||
|
// Allow insecure connections if requested
|
||||||
|
if ($this->allowInsecure) {
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ch = curl_init();
|
// Add custom headers
|
||||||
curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d%s', $containerAddress, $this->port, $this->path));
|
if (!empty($this->headers)) {
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(static fn ($k, $v) => "$k: $v", array_keys($this->headers), $this->headers));
|
||||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
|
}
|
||||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
|
||||||
|
|
||||||
curl_exec($ch);
|
curl_exec($ch);
|
||||||
|
$responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== $this->statusCode) {
|
|
||||||
throw new ContainerNotReadyException($id, new \RuntimeException('HTTP status code does not match'));
|
|
||||||
}
|
|
||||||
|
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
|
return $responseCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,42 +4,23 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Testcontainers\Wait;
|
namespace Testcontainers\Wait;
|
||||||
|
|
||||||
use Docker\Docker;
|
/**
|
||||||
use JsonException;
|
* @deprecated Use WaitForHostPort instead
|
||||||
use RuntimeException;
|
* Kept for backward compatibility
|
||||||
use Testcontainers\Exception\ContainerNotReadyException;
|
* Should be removed in next major version
|
||||||
|
*/
|
||||||
//TODO: not ready yet
|
final class WaitForTcpPortOpen extends WaitForHostPort
|
||||||
final class WaitForTcpPortOpen implements WaitStrategy
|
|
||||||
{
|
{
|
||||||
private Docker $dockerClient;
|
/**
|
||||||
|
* @phpstan-ignore-next-line
|
||||||
public function __construct(private readonly int $port, private readonly ?string $network = null)
|
*/
|
||||||
|
public function __construct(int $port, string $network = null)
|
||||||
{
|
{
|
||||||
$this->dockerClient = Docker::create();
|
parent::__construct($port);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function make(int $port, ?string $network = null): self
|
public static function make(int $port, ?string $network = null): self
|
||||||
{
|
{
|
||||||
return new self($port, $network);
|
return new self($port);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws JsonException
|
|
||||||
*/
|
|
||||||
public function wait(string $id): void
|
|
||||||
{
|
|
||||||
$containerInspectResult = $this->dockerClient->containerInspect($id);
|
|
||||||
$dockerContainerNetworks = $containerInspectResult->getNetworkSettings()->getNetworks();
|
|
||||||
$dockerContainerAddress = '';
|
|
||||||
foreach ($dockerContainerNetworks as $network) {
|
|
||||||
if ($network->getNetworkID() === $this->network) {
|
|
||||||
$dockerContainerAddress = $network->getIPAddress();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (@fsockopen($dockerContainerAddress, $this->port) === false) {
|
|
||||||
throw new ContainerNotReadyException($id, new RuntimeException('Unable to connect to container TCP port'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,6 @@ use Testcontainers\Wait\WaitForTcpPortOpen;
|
|||||||
*/
|
*/
|
||||||
class WaitStrategyTest extends TestCase
|
class WaitStrategyTest extends TestCase
|
||||||
{
|
{
|
||||||
//TODO: remove after check
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
$this->markTestIncomplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWaitForExec(): void
|
public function testWaitForExec(): void
|
||||||
{
|
{
|
||||||
$container = MySQLContainer::make()
|
$container = MySQLContainer::make()
|
||||||
@@ -86,12 +80,13 @@ class WaitStrategyTest extends TestCase
|
|||||||
public function testWaitForHTTP(): void
|
public function testWaitForHTTP(): void
|
||||||
{
|
{
|
||||||
$container = Container::make('nginx:alpine')
|
$container = Container::make('nginx:alpine')
|
||||||
->withWait(WaitForHttp::make(80));
|
->withWait(WaitForHttp::make(3000))
|
||||||
|
->withPort('3000', '80');
|
||||||
|
|
||||||
$container->run();
|
$container->run();
|
||||||
|
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80));
|
curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), $container->getPort()));
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
$response = (string) curl_exec($ch);
|
$response = (string) curl_exec($ch);
|
||||||
@@ -99,42 +94,21 @@ class WaitStrategyTest extends TestCase
|
|||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
$this->assertNotEmpty($response);
|
$this->assertNotEmpty($response);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function testWaitForTcpPortOpen(): void
|
||||||
* @dataProvider provideWaitForTcpPortOpen
|
|
||||||
*/
|
|
||||||
public function testWaitForTcpPortOpen(bool $wait): void
|
|
||||||
{
|
{
|
||||||
$container = Container::make('nginx:alpine');
|
$container = Container::make('nginx:alpine')
|
||||||
|
->withWait(WaitForTcpPortOpen::make(80))
|
||||||
if ($wait) {
|
->withPort('80', '80');
|
||||||
$container->withWait(WaitForTcpPortOpen::make(80));
|
|
||||||
}
|
|
||||||
|
|
||||||
$container->run();
|
$container->run();
|
||||||
|
|
||||||
if ($wait) {
|
static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container');
|
||||||
static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$containerId = $container->getId();
|
$container->stop();
|
||||||
|
|
||||||
$this->expectExceptionObject(new ContainerNotReadyException($containerId));
|
|
||||||
|
|
||||||
(new WaitForTcpPortOpen(8080))->wait($containerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array<bool>>
|
|
||||||
*/
|
|
||||||
public function provideWaitForTcpPortOpen(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'Can connect to container' => [true],
|
|
||||||
'Cannot connect to container' => [false],
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWaitForHealthCheck(): void
|
public function testWaitForHealthCheck(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user