mirror of
https://github.com/stan220/testcontainers-php.git
synced 2026-09-08 16:29:29 +00:00
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
use Testcontainers\Utils\PortGenerator\FixedPortGenerator;
|
||||
|
||||
/**
|
||||
* Added for backward compatibility.
|
||||
* @deprecated Use GenericContainer instead.
|
||||
@@ -52,6 +54,7 @@ class Container extends GenericContainer
|
||||
*/
|
||||
public function withPort(string $localPort, string $containerPort): self
|
||||
{
|
||||
$this->withPortGenerator(new FixedPortGenerator([(int)$localPort]));
|
||||
return $this->withExposedPorts($containerPort);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,21 @@ declare(strict_types=1);
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
use Docker\API\Exception\ContainerCreateNotFoundException;
|
||||
use Docker\API\Model\ContainerCreateResponse;
|
||||
use Docker\API\Model\ContainersCreatePostBody;
|
||||
use Docker\API\Model\EndpointSettings;
|
||||
use Docker\API\Model\HealthConfig;
|
||||
use Docker\API\Model\HostConfig;
|
||||
use Docker\API\Model\Mount;
|
||||
use Docker\API\Model\NetworkingConfig;
|
||||
use Docker\API\Model\PortBinding;
|
||||
use Docker\Docker;
|
||||
use Docker\Stream\CreateImageStream;
|
||||
use InvalidArgumentException;
|
||||
use Testcontainers\ContainerClient\DockerContainerClient;
|
||||
use Testcontainers\Utils\PortGenerator\PortGenerator;
|
||||
use Testcontainers\Utils\PortGenerator\RandomUniquePortGenerator;
|
||||
use Testcontainers\Utils\PortNormalizer;
|
||||
use Testcontainers\Wait\WaitForContainer;
|
||||
use Testcontainers\Wait\WaitStrategy;
|
||||
|
||||
@@ -40,9 +45,14 @@ class GenericContainer implements TestContainer
|
||||
|
||||
protected WaitStrategy $waitStrategy;
|
||||
|
||||
protected PortGenerator $portGenerator;
|
||||
|
||||
protected bool $isPrivileged = false;
|
||||
protected ?string $networkName = null;
|
||||
|
||||
protected int $startAttempts = 0;
|
||||
protected const MAX_START_ATTEMPTS = 2;
|
||||
|
||||
/**
|
||||
* @var array<Mount>
|
||||
*/
|
||||
@@ -55,6 +65,8 @@ class GenericContainer implements TestContainer
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->dockerClient = DockerContainerClient::getDockerClient();
|
||||
$this->waitStrategy = new WaitForContainer();
|
||||
$this->portGenerator = new RandomUniquePortGenerator();
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
@@ -111,12 +123,19 @@ class GenericContainer implements TestContainer
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withHealthCheckCommand(string $command, int $healthCheckIntervalInMS = 1000): static
|
||||
{
|
||||
$this->healthConfig = new HealthConfig([
|
||||
'Test' => ['CMD', $command],
|
||||
'Interval' => $healthCheckIntervalInMS,
|
||||
]);
|
||||
public function withHealthCheckCommand(
|
||||
string $command,
|
||||
int $intervalInMilliseconds = 1000,
|
||||
int $timeoutInMilliseconds = 3000,
|
||||
int $retries = 3,
|
||||
int $startPeriodInMilliseconds = 0
|
||||
): static {
|
||||
$this->healthConfig = new HealthConfig();
|
||||
$this->healthConfig->setTest(['CMD-SHELL', $command]);
|
||||
$this->healthConfig->setInterval($intervalInMilliseconds * 1_000_000);
|
||||
$this->healthConfig->setTimeout($timeoutInMilliseconds * 1_000_000);
|
||||
$this->healthConfig->setRetries($retries);
|
||||
$this->healthConfig->setStartPeriod($startPeriodInMilliseconds * 1_000_000);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -144,37 +163,13 @@ class GenericContainer implements TestContainer
|
||||
$this->withExposedPorts(...$port);
|
||||
} else {
|
||||
// Handle single port entry, either string or int
|
||||
$this->exposedPorts[] = $this->normalizePort($port);
|
||||
$this->exposedPorts[] = PortNormalizer::normalizePort($port);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port specification to ensure it includes a protocol.
|
||||
* Defaults to 'tcp' if no protocol is specified.
|
||||
*
|
||||
* @param string|int $port Port to normalize.
|
||||
* @return string Normalized port string.
|
||||
*
|
||||
* TODO: move this to a utility class
|
||||
*/
|
||||
private function normalizePort(string|int $port): string
|
||||
{
|
||||
if (is_int($port)) {
|
||||
// Direct integer ports default to tcp
|
||||
return "{$port}/tcp";
|
||||
}
|
||||
|
||||
// Check if the port specification already includes a protocol
|
||||
if (is_string($port) && !str_contains($port, '/')) {
|
||||
return "{$port}/tcp";
|
||||
}
|
||||
|
||||
return $port;
|
||||
}
|
||||
|
||||
public function withPrivilegedMode(bool $privileged = true): static
|
||||
{
|
||||
$this->isPrivileged = $privileged;
|
||||
@@ -190,66 +185,125 @@ class GenericContainer implements TestContainer
|
||||
return $this;
|
||||
}
|
||||
|
||||
//TODO: needs refactoring
|
||||
public function withPortGenerator(PortGenerator $portGenerator): static
|
||||
{
|
||||
$this->portGenerator = $portGenerator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function start(): StartedGenericContainer
|
||||
{
|
||||
$this->startAttempts++;
|
||||
$containerConfig = $this->createContainerConfig();
|
||||
try {
|
||||
$containerCreatePostBody = new ContainersCreatePostBody();
|
||||
//handle withExposedPorts
|
||||
if (!empty($this->exposedPorts)) {
|
||||
$portGenerator = new RandomUniquePortGenerator();
|
||||
$portMap = new \ArrayObject();
|
||||
|
||||
foreach ($this->exposedPorts as $port) {
|
||||
$portBinding = new PortBinding();
|
||||
$portBinding->setHostPort((string) $portGenerator->generatePort());
|
||||
$portBinding->setHostIp('0.0.0.0');
|
||||
$portMap[$port] = [$portBinding];
|
||||
}
|
||||
|
||||
$hostConfig = new HostConfig();
|
||||
$hostConfig->setPortBindings($portMap);
|
||||
//handle withPrivilegedMode
|
||||
if ($this->isPrivileged) {
|
||||
$hostConfig->setPrivileged($this->isPrivileged);
|
||||
}
|
||||
$containerCreatePostBody->setHostConfig($hostConfig);
|
||||
}
|
||||
//handle withPrivilegedMode
|
||||
if ($this->isPrivileged) {
|
||||
$hostConfig = new HostConfig();
|
||||
$hostConfig->setPrivileged($this->isPrivileged);
|
||||
}
|
||||
$containerCreatePostBody->setImage($this->image);
|
||||
$containerCreatePostBody->setCmd($this->command);
|
||||
$envs = [];
|
||||
foreach ($this->env as $key => $value) {
|
||||
$envs[] = $key . '=' . $value;
|
||||
}
|
||||
$containerCreatePostBody->setEnv($envs);
|
||||
|
||||
$containerCreateResponse = $this->dockerClient->containerCreate($containerCreatePostBody);
|
||||
/** @var ContainerCreateResponse|null $containerCreateResponse */
|
||||
$containerCreateResponse = $this->dockerClient->containerCreate($containerConfig);
|
||||
$this->id = $containerCreateResponse?->getId() ?? '';
|
||||
} catch (ContainerCreateNotFoundException) {
|
||||
/** @var CreateImageStream $imageCreateResponse */
|
||||
$imageCreateResponse = $this->dockerClient->imageCreate(null, [
|
||||
'fromImage' => explode(':', $this->image)[0],
|
||||
'tag' => explode(':', $this->image)[1] ?? 'latest',
|
||||
]);
|
||||
$imageCreateResponse->wait();
|
||||
|
||||
if ($this->startAttempts >= self::MAX_START_ATTEMPTS) {
|
||||
throw new \RuntimeException("Failed to start container after pulling image.");
|
||||
}
|
||||
// If the image is not found, pull it and try again
|
||||
// TODO: add withPullPolicy support
|
||||
$this->pullImage();
|
||||
return $this->start();
|
||||
}
|
||||
|
||||
$this->dockerClient->containerStart($this->id);
|
||||
|
||||
if (!isset($this->waitStrategy)) {
|
||||
$this->withWait(new WaitForContainer());
|
||||
}
|
||||
|
||||
$startedContainer = new StartedGenericContainer($this->id);
|
||||
$this->waitStrategy->wait($startedContainer);
|
||||
|
||||
return $startedContainer;
|
||||
}
|
||||
|
||||
protected function createContainerConfig(): ContainersCreatePostBody
|
||||
{
|
||||
$containerCreatePostBody = new ContainersCreatePostBody();
|
||||
$containerCreatePostBody->setImage($this->image);
|
||||
$containerCreatePostBody->setCmd($this->command);
|
||||
|
||||
$envs = array_map(static fn ($key, $value) => "$key=$value", array_keys($this->env), $this->env);
|
||||
$containerCreatePostBody->setEnv($envs);
|
||||
|
||||
$hostConfig = $this->createHostConfig();
|
||||
$containerCreatePostBody->setHostConfig($hostConfig);
|
||||
|
||||
if ($this->entryPoint !== null) {
|
||||
$containerCreatePostBody->setEntrypoint([$this->entryPoint]);
|
||||
}
|
||||
|
||||
if ($this->healthConfig !== null) {
|
||||
$containerCreatePostBody->setHealthcheck($this->healthConfig);
|
||||
}
|
||||
|
||||
if ($this->networkName !== null) {
|
||||
$networkingConfig = new NetworkingConfig();
|
||||
$endpointsConfig = new \ArrayObject([
|
||||
$this->networkName => new EndpointSettings(),
|
||||
]);
|
||||
$networkingConfig->setEndpointsConfig($endpointsConfig);
|
||||
$containerCreatePostBody->setNetworkingConfig($networkingConfig);
|
||||
}
|
||||
|
||||
return $containerCreatePostBody;
|
||||
}
|
||||
|
||||
protected function createHostConfig(): ?HostConfig
|
||||
{
|
||||
/**
|
||||
* For some reason, if some of the properties are not set, but HostConfig is returned,
|
||||
* the API will throw ContainerCreateBadRequestException: bad parameter.
|
||||
* Until it will be checked and fixed, we just return null if these properties are not set.
|
||||
* */
|
||||
if ($this->exposedPorts === [] && !$this->isPrivileged && $this->mounts === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hostConfig = new HostConfig();
|
||||
|
||||
if ($this->exposedPorts !== []) {
|
||||
$portBindings = $this->createPortBindings();
|
||||
$hostConfig->setPortBindings($portBindings);
|
||||
}
|
||||
|
||||
if ($this->isPrivileged) {
|
||||
$hostConfig->setPrivileged(true);
|
||||
}
|
||||
|
||||
if ($this->mounts !== []) {
|
||||
$hostConfig->setMounts($this->mounts);
|
||||
}
|
||||
|
||||
return $hostConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, PortBinding>>
|
||||
*/
|
||||
protected function createPortBindings(): array
|
||||
{
|
||||
$portBindings = [];
|
||||
|
||||
foreach ($this->exposedPorts as $port) {
|
||||
$portBinding = new PortBinding();
|
||||
$portBinding->setHostPort((string)$this->portGenerator->generatePort());
|
||||
$portBinding->setHostIp('0.0.0.0');
|
||||
$portBindings[$port] = [$portBinding];
|
||||
}
|
||||
|
||||
return $portBindings;
|
||||
}
|
||||
|
||||
protected function pullImage(): void
|
||||
{
|
||||
[$fromImage, $tag] = explode(':', $this->image) + [1 => 'latest'];
|
||||
/** @var CreateImageStream $imageCreateResponse */
|
||||
$imageCreateResponse = $this->dockerClient->imageCreate(null, [
|
||||
'fromImage' => $fromImage,
|
||||
'tag' => $tag,
|
||||
]);
|
||||
$imageCreateResponse->wait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
/**
|
||||
* The IP protocols supported by Docker.
|
||||
*/
|
||||
enum InternetProtocol: string
|
||||
{
|
||||
case TCP = 'TCP';
|
||||
case UDP = 'UDP';
|
||||
|
||||
public function toDockerNotation(): string
|
||||
{
|
||||
return strtolower($this->value);
|
||||
}
|
||||
|
||||
public static function fromDockerNotation(string $protocol): self
|
||||
{
|
||||
return self::from(strtoupper($protocol));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
use Testcontainers\Utils\PortGenerator\FixedPortGenerator;
|
||||
use Testcontainers\Wait\WaitForExec;
|
||||
|
||||
/**
|
||||
@@ -16,6 +17,7 @@ class MariaDBContainer extends Container
|
||||
public function __construct(string $version = 'latest', string $mysqlRootPassword = 'root')
|
||||
{
|
||||
parent::__construct('mariadb:' . $version);
|
||||
$this->withPortGenerator(new FixedPortGenerator([3306]));
|
||||
$this->withExposedPorts(3306);
|
||||
$this->withEnvironment('MARIADB_ROOT_PASSWORD', $mysqlRootPassword);
|
||||
$this->withWait(new WaitForExec([
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
use Testcontainers\Utils\PortGenerator\FixedPortGenerator;
|
||||
use Testcontainers\Wait\WaitForExec;
|
||||
|
||||
/**
|
||||
@@ -16,6 +17,7 @@ class MySQLContainer extends Container
|
||||
public function __construct(string $version = 'latest', string $mysqlRootPassword = 'root')
|
||||
{
|
||||
parent::__construct('mysql:' . $version);
|
||||
$this->withPortGenerator(new FixedPortGenerator([3306]));
|
||||
$this->withExposedPorts(3306);
|
||||
$this->withEnvironment('MYSQL_ROOT_PASSWORD', $mysqlRootPassword);
|
||||
$this->withWait(new WaitForExec([
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
use Testcontainers\Utils\PortGenerator\FixedPortGenerator;
|
||||
use Testcontainers\Wait\WaitForLog;
|
||||
|
||||
/**
|
||||
@@ -16,6 +17,7 @@ class OpenSearchContainer extends Container
|
||||
public function __construct(string $version = 'latest')
|
||||
{
|
||||
parent::__construct('opensearchproject/opensearch:' . $version);
|
||||
$this->withPortGenerator(new FixedPortGenerator([9200]));
|
||||
$this->withExposedPorts(9200);
|
||||
$this->withEnvironment('discovery.type', 'single-node');
|
||||
$this->withEnvironment('OPENSEARCH_INITIAL_ADMIN_PASSWORD', 'c3o_ZPHo!');
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
use Testcontainers\Utils\PortGenerator\FixedPortGenerator;
|
||||
use Testcontainers\Wait\WaitForExec;
|
||||
|
||||
/**
|
||||
@@ -20,6 +21,7 @@ class PostgresContainer extends Container
|
||||
public readonly string $database = 'test'
|
||||
) {
|
||||
parent::__construct('postgres:' . $version);
|
||||
$this->withPortGenerator(new FixedPortGenerator([5432]));
|
||||
$this->withExposedPorts(5432);
|
||||
$this->withEnvironment('POSTGRES_USER', $this->username);
|
||||
$this->withEnvironment('POSTGRES_PASSWORD', $this->password);
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
use Testcontainers\Utils\PortGenerator\FixedPortGenerator;
|
||||
use Testcontainers\Wait\WaitForLog;
|
||||
|
||||
/**
|
||||
@@ -16,6 +17,7 @@ class RedisContainer extends Container
|
||||
public function __construct(string $version = 'latest')
|
||||
{
|
||||
parent::__construct('redis:' . $version);
|
||||
$this->withPortGenerator(new FixedPortGenerator([6379]));
|
||||
$this->withExposedPorts(6379);
|
||||
$this->withWait(new WaitForLog('Ready to accept connections'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Exception;
|
||||
|
||||
class ContainerException extends \RuntimeException
|
||||
{
|
||||
protected string $containerId;
|
||||
|
||||
public function __construct(string $message, string $containerId = '', ?\Throwable $previous = null)
|
||||
{
|
||||
$this->containerId = $containerId;
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
public function getContainerId(): string
|
||||
{
|
||||
return $this->containerId;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Exception;
|
||||
|
||||
class ContainerNotReadyException extends \RuntimeException
|
||||
class ContainerNotReadyException extends ContainerException
|
||||
{
|
||||
public function __construct(string $id, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(sprintf('Container %s is not ready', $id), 0, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Exception;
|
||||
|
||||
class ContainerStateException extends ContainerException
|
||||
{
|
||||
public function __construct(string $containerId, ?\Throwable $previous = null)
|
||||
{
|
||||
$message = sprintf('Unable to retrieve state for container %s', $containerId);
|
||||
parent::__construct($message, $containerId, $previous);
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Exception;
|
||||
|
||||
class ContainerWaitingTimeoutException extends \RuntimeException
|
||||
class ContainerWaitingTimeoutException extends ContainerNotReadyException
|
||||
{
|
||||
protected string $containerId;
|
||||
|
||||
public function __construct(string $containerId, ?string $message = null, ?\Throwable $previous = null)
|
||||
{
|
||||
$this->containerId = $containerId;
|
||||
$message ??= sprintf('Timeout reached while waiting for container %s', $containerId);
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
public function getContainerId(): string
|
||||
{
|
||||
return $this->containerId;
|
||||
parent::__construct($message, $containerId, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Exception;
|
||||
|
||||
class HealthCheckFailedException extends ContainerNotReadyException
|
||||
{
|
||||
public function __construct(string $containerId, ?\Throwable $previous = null)
|
||||
{
|
||||
$message = sprintf('Health check failed: Container %s is unhealthy', $containerId);
|
||||
parent::__construct($message, $containerId, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Exception;
|
||||
|
||||
class HealthCheckNotConfiguredException extends ContainerNotReadyException
|
||||
{
|
||||
public function __construct(string $containerId, ?\Throwable $previous = null)
|
||||
{
|
||||
$message = sprintf('Health check not configured for container %s', $containerId);
|
||||
parent::__construct($message, $containerId, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Exception;
|
||||
|
||||
class UnknownHealthStatusException extends ContainerNotReadyException
|
||||
{
|
||||
public function __construct(string $containerId, string $status, ?\Throwable $previous = null)
|
||||
{
|
||||
$message = sprintf('Unknown health status %s for container %s', $status, $containerId);
|
||||
parent::__construct($message, $containerId, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Utils;
|
||||
|
||||
use Testcontainers\Container\InternetProtocol;
|
||||
|
||||
class PortNormalizer
|
||||
{
|
||||
/**
|
||||
* Normalize a port specification to ensure it includes a protocol.
|
||||
* Defaults to 'tcp' if no protocol is specified.
|
||||
*
|
||||
* @param string|int $port Port to normalize.
|
||||
* @return string Normalized port string.
|
||||
*/
|
||||
public static function normalizePort(string|int $port, InternetProtocol $internetProtocol = InternetProtocol::TCP): string
|
||||
{
|
||||
if (is_int($port)) {
|
||||
// Direct integer ports default to tcp
|
||||
return "{$port}/{$internetProtocol->toDockerNotation()}";
|
||||
}
|
||||
|
||||
// Check if the port specification already includes a protocol
|
||||
if (is_string($port) && !str_contains($port, '/')) {
|
||||
return "{$port}/{$internetProtocol->toDockerNotation()}";
|
||||
}
|
||||
|
||||
return $port;
|
||||
}
|
||||
}
|
||||
@@ -4,43 +4,70 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Wait;
|
||||
|
||||
use Docker\Docker;
|
||||
use Http\Client\Socket\Exception\TimeoutException;
|
||||
use Docker\API\Model\ContainersIdJsonGetResponse200;
|
||||
use Testcontainers\Container\StartedTestContainer;
|
||||
use Testcontainers\Exception\ContainerNotReadyException;
|
||||
use Testcontainers\Exception\ContainerStateException;
|
||||
use Testcontainers\Exception\ContainerWaitingTimeoutException;
|
||||
use Testcontainers\Exception\HealthCheckFailedException;
|
||||
use Testcontainers\Exception\HealthCheckNotConfiguredException;
|
||||
use Testcontainers\Exception\UnknownHealthStatusException;
|
||||
|
||||
//TODO: not ready yet
|
||||
/**
|
||||
* Wait strategy that waits until the container's health status is 'healthy'.
|
||||
*
|
||||
* Possible health statuses:
|
||||
* - "none": No health check configured.
|
||||
* - "starting": Health check is in progress.
|
||||
* - "healthy": Container is healthy.
|
||||
* - "unhealthy": Container is unhealthy.
|
||||
*/
|
||||
class WaitForHealthCheck extends BaseWaitStrategy
|
||||
{
|
||||
public function __construct(protected int $timeout = 5000, protected int $pollInterval = 1000)
|
||||
{
|
||||
parent::__construct($timeout, $pollInterval);
|
||||
}
|
||||
|
||||
public function wait(StartedTestContainer $container): void
|
||||
{
|
||||
$startTime = microtime(true) * 1000;
|
||||
$startTime = microtime(true);
|
||||
|
||||
while (true) {
|
||||
$elapsedTime = (microtime(true) * 1000) - $startTime;
|
||||
$elapsedTime = (microtime(true) - $startTime) * 1000;
|
||||
|
||||
if ($elapsedTime > $this->timeout) {
|
||||
throw new TimeoutException(sprintf("Health check not healthy after %d ms", $this->timeout));
|
||||
throw new ContainerWaitingTimeoutException($container->getId());
|
||||
}
|
||||
|
||||
/** @var \Psr\Http\Message\ResponseInterface | null $containerInspect */
|
||||
$containerInspect = $container->getClient()->containerInspect($container->getId(), [], Docker::FETCH_RESPONSE);
|
||||
//$containerStatus = $containerInspect?->getArrayCopy() ?? null;
|
||||
$containerStatus = '';
|
||||
if ($containerStatus === 'healthy') {
|
||||
return;
|
||||
/** @var ContainersIdJsonGetResponse200|null $containerInspect */
|
||||
$containerInspect = $container->getClient()->containerInspect($container->getId());
|
||||
|
||||
$containerState = $containerInspect?->getState();
|
||||
|
||||
if ($containerState !== null) {
|
||||
$health = $containerState->getHealth();
|
||||
|
||||
if ($health !== null) {
|
||||
$status = $health->getStatus();
|
||||
|
||||
switch ($status) {
|
||||
case 'healthy':
|
||||
return; // Container is healthy
|
||||
case 'starting':
|
||||
// Health check is still in progress; continue waiting
|
||||
break;
|
||||
case 'unhealthy':
|
||||
throw new HealthCheckFailedException($container->getId());
|
||||
case 'none':
|
||||
throw new HealthCheckNotConfiguredException($container->getId());
|
||||
default:
|
||||
throw new UnknownHealthStatusException($container->getId(), (string)$status);
|
||||
}
|
||||
} else {
|
||||
// Health is null; treat as 'none' status
|
||||
throw new HealthCheckNotConfiguredException($container->getId());
|
||||
}
|
||||
} else {
|
||||
// Container state is null
|
||||
throw new ContainerStateException($container->getId());
|
||||
}
|
||||
|
||||
if ($containerStatus === 'unhealthy') {
|
||||
throw new ContainerNotReadyException(sprintf("Health check failed: %s", $containerStatus));
|
||||
}
|
||||
|
||||
usleep($this->pollInterval * 1000); // Sleep for the polling interval
|
||||
usleep($this->pollInterval * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,6 @@ use Testcontainers\Container\RedisContainer;
|
||||
*/
|
||||
class ContainerTest extends TestCase
|
||||
{
|
||||
//TODO: remove after check
|
||||
//To make it work, fixed port should be first implemented
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->markTestIncomplete();
|
||||
}
|
||||
|
||||
public function testMySQL(): void
|
||||
{
|
||||
$container = MySQLContainer::make();
|
||||
@@ -117,6 +110,8 @@ class ContainerTest extends TestCase
|
||||
$this->assertArrayHasKey('cluster_name', $data);
|
||||
|
||||
$this->assertEquals('docker-cluster', $data['cluster_name']);
|
||||
|
||||
$container->stop();
|
||||
}
|
||||
|
||||
public function testPostgreSQLContainer(): void
|
||||
|
||||
@@ -8,7 +8,8 @@ use PHPUnit\Framework\TestCase;
|
||||
use Predis\Client;
|
||||
use Predis\Connection\ConnectionException;
|
||||
use Testcontainers\Container\Container;
|
||||
use Testcontainers\Exception\ContainerNotReadyException;
|
||||
use Testcontainers\Container\MySQLContainer;
|
||||
use Testcontainers\Container\RedisContainer;
|
||||
use Testcontainers\Wait\WaitForExec;
|
||||
use Testcontainers\Wait\WaitForHealthCheck;
|
||||
use Testcontainers\Wait\WaitForHttp;
|
||||
@@ -28,7 +29,7 @@ class WaitStrategyTest extends TestCase
|
||||
|
||||
public function testWaitForExec(): void
|
||||
{
|
||||
$container = Container::make('mysql')
|
||||
$container = MySQLContainer::make()
|
||||
->withEnvironment('MYSQL_ROOT_PASSWORD', 'root')
|
||||
->withWait(
|
||||
new WaitForExec([
|
||||
@@ -52,11 +53,13 @@ class WaitStrategyTest extends TestCase
|
||||
$version = $query->fetchColumn();
|
||||
|
||||
$this->assertNotEmpty($version);
|
||||
|
||||
$container->stop();
|
||||
}
|
||||
|
||||
public function testWaitForLog(): void
|
||||
{
|
||||
$container = Container::make('redis:6.2.5')
|
||||
$container = RedisContainer::make()
|
||||
->withWait(new WaitForLog('Ready to accept connections'));
|
||||
|
||||
$container->run();
|
||||
@@ -138,6 +141,7 @@ class WaitStrategyTest extends TestCase
|
||||
{
|
||||
$container = Container::make('nginx')
|
||||
->withHealthCheckCommand('curl --fail http://localhost')
|
||||
->withPort('80', '80')
|
||||
->withWait(new WaitForHealthCheck());
|
||||
|
||||
$container->run();
|
||||
@@ -153,5 +157,7 @@ class WaitStrategyTest extends TestCase
|
||||
$this->assertIsString($response);
|
||||
|
||||
$this->assertStringContainsString('Welcome to nginx!', $response);
|
||||
|
||||
$container->stop();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user