implement WaitForHttp and WaitForHostPort strategies

This commit is contained in:
Sergei Shitikov
2024-10-24 19:36:48 +02:00
parent a5c8d2bb73
commit 1cbfe638b9
5 changed files with 195 additions and 114 deletions
+12
View File
@@ -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;
}
} }
+51
View File
@@ -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;
}
}
+106 -43
View File
@@ -4,83 +4,146 @@ 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);
}
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(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d%s', $containerAddress, $this->port, $this->path)); curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method->value);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_NOBODY, 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);
}
// Add custom headers
if (!empty($this->headers)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(static fn ($k, $v) => "$k: $v", array_keys($this->headers), $this->headers));
}
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;
} }
} }
+12 -31
View File
@@ -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'));
}
} }
} }
+10 -36
View File
@@ -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