diff --git a/README.md b/README.md index 3e2c2e1..22cf3e0 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ composer req --dev testcontainers/testcontainers use Testcontainers\Container\GenericContainer; -$container = GenericContainer::make('nginx:alpine'); +$container = new GenericContainer::make('nginx:alpine'); // set an environment variable $container->withEnvironment('name', 'var'); @@ -58,7 +58,7 @@ $container->withWait(new WaitForHealthCheck()); ```php withMySQLDatabase('foo'); @@ -80,7 +80,7 @@ $pdo = new \PDO( ```php withMariaDBDatabase('foo'); @@ -102,7 +102,7 @@ $pdo = new \PDO( ```php withPostgresDatabase('database'); @@ -123,7 +123,7 @@ $pdo = new \PDO( ```php -use Testcontainers\Container\RedisContainer; +use Testcontainers\Modules\RedisContainer; $container = RedisContainer::make('6.0'); @@ -139,7 +139,7 @@ $redis->connect($container->getAddress()); ```php -use Testcontainers\Container\OpenSearchContainer; +use Testcontainers\Modules\OpenSearchContainer; $container = OpenSearchContainer::make('2'); $container->disableSecurityPlugin(); @@ -166,7 +166,7 @@ use Doctrine\Bundle\DoctrineBundle\ConnectionFactory; use Doctrine\Common\EventManager; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Tools\DsnParser; -use Testcontainers\Container\PostgresContainer; +use Testcontainers\Modules\PostgresContainer; class TestConnectionFactory extends ConnectionFactory { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd48e82..9993376 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,6 @@ tests - tests/Integration/WaitStrategyTest.php diff --git a/src/Container/Container.php b/src/Container/Container.php index 0134203..29a3d71 100644 --- a/src/Container/Container.php +++ b/src/Container/Container.php @@ -6,9 +6,154 @@ namespace Testcontainers\Container; /** * Added for backward compatibility. - * Just a wrapper for GenericContainer. * @deprecated Use GenericContainer instead. + * TODO: Remove in next major release. */ -class Container extends GenericContainer +final class Container extends GenericContainer { + protected ?StartedTestContainer $startedContainer = null; + + protected ?StoppedTestContainer $stoppedContainer = null; + public static function make(string $image): self + { + return new self($image); + } + + /** + * @deprecated Use `withPrivilegedMode` instead + */ + public function withPrivileged(bool $privileged = true): self + { + return $this->withPrivilegedMode($privileged); + } + + /** + * @deprecated Use `withExposedPorts` instead + */ + public function withPort(string $localPort, string $containerPort): self + { + return $this->withExposedPorts($containerPort); + } + + /** + * @deprecated there will be no replacement + */ + public function withImage(string $image): self + { + $this->image = $image; + + return $this; + } + + /** + * @deprecated Use `start` instead + */ + public function run(): self + { + $this->startedContainer = $this->start(); + + return $this; + } + + /** + * @param array $commandAsArray + * @deprecated Use 'exec' from StartedTestContainer instead + */ + public function execute(array $commandAsArray): string + { + if($this->startedContainer === null) { + throw new \RuntimeException('Container is not started'); + } + + return $this->startedContainer->exec($commandAsArray); + } + + /** + * @deprecated Use 'logs' from StartedTestContainer instead + */ + public function logs(): string + { + if($this->startedContainer === null) { + throw new \RuntimeException('Container is not started'); + } + + return $this->startedContainer->logs(); + } + + /** + * @deprecated Use 'getHost' from StartedTestContainer instead + */ + public function getAddress(): string + { + if($this->startedContainer === null) { + throw new \RuntimeException('Container is not started'); + } + + return $this->startedContainer->getHost(); + } + + /** + * @deprecated Use 'getFirstMappedPort' from StartedTestContainer instead + */ + public function getPort(): int + { + if($this->startedContainer === null) { + throw new \RuntimeException('Container is not started'); + } + + return $this->startedContainer->getFirstMappedPort(); + } + + /** + * @deprecated Use 'stop' from StartedTestContainer instead + */ + public function kill(): self + { + $this->dockerClient->containerKill($this->id); + + return $this; + } + + /** + * @deprecated Use `stop` from StartedTestContainer instead + */ + public function stop(): self + { + if($this->startedContainer === null) { + throw new \RuntimeException('Container is not started'); + } + + $this->stoppedContainer = $this->startedContainer->stop(); + + return $this; + } + + /** + * @deprecated Use 'restart' method from StartedTestContainer instead + */ + public function restart(): self + { + if($this->startedContainer === null) { + throw new \RuntimeException('Container is not started'); + } + + $restartedTestContainer = $this->startedContainer->restart(); + $this->startedContainer = $restartedTestContainer; + + return $this; + } + + /** + * @deprecated Use 'stop' method from StartedTestContainer instead + */ + public function remove(): self + { + if($this->startedContainer === null) { + throw new \RuntimeException('Container is not started'); + } + + $this->startedContainer->stop(); + + return $this; + } } diff --git a/src/Container/GenericContainer.php b/src/Container/GenericContainer.php index 04e26b2..6517091 100644 --- a/src/Container/GenericContainer.php +++ b/src/Container/GenericContainer.php @@ -4,39 +4,28 @@ declare(strict_types=1); namespace Testcontainers\Container; -use Docker\API\Client; use Docker\API\Exception\ContainerCreateNotFoundException; use Docker\API\Model\ContainersCreatePostBody; -use Docker\API\Model\ContainersIdExecPostBody; use Docker\API\Model\HealthConfig; use Docker\API\Model\HostConfig; use Docker\API\Model\Mount; use Docker\API\Model\PortBinding; use Docker\Docker; -use Psr\Http\Message\ResponseInterface; -use Testcontainers\ContainerRuntime\ContainerRuntimeClient; -use Testcontainers\Wait\WaitForContainerRunning; -use Testcontainers\Wait\WaitInterface; +use InvalidArgumentException; +use Testcontainers\ContainerClient\DockerContainerClient; +use Testcontainers\Utils\PortGenerator\RandomUniquePortGenerator; +use Testcontainers\Wait\WaitForContainer; +use Testcontainers\Wait\WaitStrategy; -/** - * @phpstan-type ContainerInspectSingleNetwork array - * @phpstan-type ContainerInspectMultipleNetworks array}}> - * @phpstan-type ContainerInspect ContainerInspectSingleNetwork|ContainerInspectMultipleNetworks - * @phpstan-type DockerNetwork array{CreatedAt: string, Driver: string, ID: string, IPv6: string, Internal: string, Labels: string, Name: string, Scope: string} - */ -class GenericContainer +class GenericContainer implements TestContainer { protected Docker $dockerClient; - protected ContainersCreatePostBody $containerConfig; - protected string $image; - protected string $containerName; - protected string $id; - /** @var array */ + /** @var list */ protected array $command = []; protected ?string $entryPoint = null; @@ -48,7 +37,7 @@ class GenericContainer */ protected array $env = []; - protected WaitInterface $wait; + protected WaitStrategy $waitStrategy; protected bool $isPrivileged = false; protected ?string $networkName = null; @@ -64,16 +53,7 @@ class GenericContainer public function __construct(string $image) { $this->image = $image; - $this->dockerClient = ContainerRuntimeClient::getDockerClient(); - } - - /** - * @deprecated Use constructor instead - * Left for backward compatibility - */ - public static function make(string $image): self - { - return new GenericContainer($image); + $this->dockerClient = DockerContainerClient::getDockerClient(); } public function getId(): string @@ -81,58 +61,56 @@ class GenericContainer return $this->id; } - public function withCommand(array $command): self + /** + * @param list $command + */ + public function withCommand(array $command): static { $this->command = $command; + return $this; } - public function exec(array $command): string - { - $execConfig = (new ContainersIdExecPostBody()) - ->setCmd($command) - ->setAttachStdout(true) - ->setAttachStderr(true); - - // Create and start the exec command - $exec = $this->dockerClient->containerExec($this->id, $execConfig); - $contents = $this->dockerClient - ->execStart($exec->getId(), null, Client::FETCH_RESPONSE) - ?->getBody() - ->getContents() ?? ''; - - return preg_replace('/[\x00-\x1F\x7F]/u', '', $contents); - } - - public function withEntryPoint(string $entryPoint): self + public function withEntryPoint(string $entryPoint): static { $this->entryPoint = $entryPoint; return $this; } - public function withEnvironment(string $name, string $value): self + /** + * To support temporarily backwards compatibility, the method supports two formats: + * 1. A single key-value pair (deprecated): $object->withEnvironment('key', 'value'); + * 2. An array of key-value pairs: $object->withEnvironment(['key1' => 'value1', 'key2' => 'value2']); + * + * @param string | array $env An array of environment variables or the name of a single variable. + * @param string|null $value The value of the environment variable if a single variable is passed. + * @return static Returns itself for chaining purposes. + */ + public function withEnvironment(string | array $env, ?string $value = null): static { - $this->env[$name] = $value; + if (is_array($env)) { + foreach ($env as $key => $val) { + $this->env[$key] = $val; + } + } else { + if ($value === null) { + throw new InvalidArgumentException("Value cannot be null when setting a single environment variable."); + } + $this->env[$env] = $value; + } return $this; } - public function withImage(string $image): self + public function withWait(WaitStrategy $waitStrategy): static { - $this->image = $image; + $this->waitStrategy = $waitStrategy; return $this; } - public function withWait(WaitInterface $wait): self - { - $this->wait = $wait; - - return $this; - } - - public function withHealthCheckCommand(string $command, int $healthCheckIntervalInMS = 1000): self + public function withHealthCheckCommand(string $command, int $healthCheckIntervalInMS = 1000): static { $this->healthConfig = new HealthConfig([ 'Test' => ['CMD', $command], @@ -142,33 +120,22 @@ class GenericContainer return $this; } - public function withMount(string $localPath, string $containerPath): self + public function withMount(string $localPath, string $containerPath): static { $this->mounts[] = new Mount(['type' => 'bind', 'source' => $localPath, 'target' => $containerPath]); return $this; } - /** - * @deprecated Use `withExposedPorts` instead - */ - public function withPort(string $localPort, string $containerPort): self - { - return $this->withExposedPorts($containerPort); - } - - /** - * @psalm-param string|int|array $port - */ /** * Add ports to be exposed by the Docker container. * This method accepts multiple inputs: single port, multiple ports, or ports with specific protocols * to attempt to align with other language implementations. * * @psalm-param int|string|array $ports One or more ports to expose. - * @return self Fluent interface for chaining. + * @return static Fluent interface for chaining. */ - public function withExposedPorts(...$ports): self + public function withExposedPorts(...$ports): static { foreach ($ports as $port) { if (is_array($port)) { @@ -207,46 +174,51 @@ class GenericContainer return $port; } - public function withPrivileged(bool $privileged = true): self + public function withPrivilegedMode(bool $privileged = true): static { $this->isPrivileged = $privileged; return $this; } - public function withNetwork(string $networkName): self + //TODO: not yet implemented + public function withNetwork(string $networkName): static { $this->networkName = $networkName; return $this; } - public function stop(): self - { - $this->dockerClient->containerStop($this->id); - - return $this; - } - - public function start(): self + //TODO: needs refactoring + public function start(): StartedGenericContainer { try { $containerCreatePostBody = new ContainersCreatePostBody(); - //setup only if we need to expose ports + //handle withExposedPorts if(!empty($this->exposedPorts)) { + $portGenerator = new RandomUniquePortGenerator(); $portMap = new \ArrayObject(); foreach ($this->exposedPorts as $port) { $portBinding = new PortBinding(); - $portBinding->setHostPort(explode('/', $port)[0]); + $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 = []; @@ -267,96 +239,13 @@ class GenericContainer $this->dockerClient->containerStart($this->id); - if(!isset($this->wait)) { - $this->withWait(new WaitForContainerRunning()); + if(!isset($this->waitStrategy)) { + $this->withWait(new WaitForContainer()); } - $this->wait->wait($this->id); + $startedContainer = new StartedGenericContainer($this->id); + $this->waitStrategy->wait($startedContainer); - return $this; - } - - public function restart(): self - { - $this->dockerClient->containerRestart($this->id); - - return $this; - } - - public function remove(): self - { - $this->dockerClient->containerStop($this->id); - $this->dockerClient->containerDelete($this->id); - - return $this; - } - - public function kill(): self - { - $this->dockerClient->containerKill($this->id); - - return $this; - } - - /** - * @deprecated Use `start` instead - * Left for backward compatibility - */ - public function run(): self - { - return $this->start(); - } - - /** - * @param array $commandAsArray - */ - public function execute(array $commandAsArray): ResponseInterface - { - $command = new ContainersIdExecPostBody(); - $command->setCmd($commandAsArray); - return $this->dockerClient->containerExec($this->id, $command); - } - - public function logs(): string - { - return $this->dockerClient->containerLogs($this->id)?->getBody()?->getContents() ?? ''; - } - - public function getAddress(): string - { - $inspection = $this->inspect(); - return $inspection['gateway']; - // foreach ($containerNetworks as $network) { - // var_dump($network->getNetworkID(), $this->id, $network->getIPAddress()); - // if($network->getNetworkID() === $this->id) { - // $containerAddress = $network->getIpAddress(); - // break; - // } - // } - // return $containerAddress; - } - - /** - * @return array{gateway: string, ports: array} - */ - public function inspect(): array - { - $response = $this->dockerClient->containerInspect($this->id); - $settings = $response->getNetworkSettings(); - //var_dump($settings); - - $ports = []; - foreach ($settings->getPorts() as $port => $value) { - if ($value === null) { - continue; - } - - $ports[$port] = (int) $value[0]->getHostPort(); - } - - return [ - 'gateway' => $settings->getGateway(), - 'ports' => $ports, - ]; + return $startedContainer; } } diff --git a/src/Container/MariaDBContainer.php b/src/Container/MariaDBContainer.php index 33a4cc2..1171ca1 100644 --- a/src/Container/MariaDBContainer.php +++ b/src/Container/MariaDBContainer.php @@ -4,39 +4,12 @@ declare(strict_types=1); namespace Testcontainers\Container; -use Testcontainers\Wait\WaitForLog; - -class MariaDBContainer extends GenericContainer +/** + * Left for namespace backward compatibility + * @deprecated Use \Testcontainers\Modules\MariaDBContainer instead. + * TODO: Remove in next major release. + */ +class MariaDBContainer extends \Testcontainers\Modules\MariaDBContainer { - public function __construct(string $version = 'latest', string $mysqlRootPassword = 'root') - { - parent::__construct('mariadb:' . $version); - $this->withExposedPorts(3306); - $this->withWait(new WaitForLog('ready for connections')); - $this->withEnvironment('MARIADB_ROOT_PASSWORD', $mysqlRootPassword); - } - /** - * @deprecated Use constructor instead - * Left for backward compatibility - */ - public static function make(string $version = 'latest', string $mysqlRootPassword = 'root'): self - { - return new self($version, $mysqlRootPassword); - } - - public function withMariaDBUser(string $username, string $password): self - { - $this->withEnvironment('MARIADB_USER', $username); - $this->withEnvironment('MARIADB_PASSWORD', $password); - - return $this; - } - - public function withMariaDBDatabase(string $database): self - { - $this->withEnvironment('MARIADB_DATABASE', $database); - - return $this; - } -} +} \ No newline at end of file diff --git a/src/Container/MySQLContainer.php b/src/Container/MySQLContainer.php index e4bb4d9..9aab957 100644 --- a/src/Container/MySQLContainer.php +++ b/src/Container/MySQLContainer.php @@ -4,39 +4,12 @@ declare(strict_types=1); namespace Testcontainers\Container; -use Testcontainers\Wait\WaitForLog; - -class MySQLContainer extends GenericContainer +/** + * Left for namespace backward compatibility + * @deprecated Use \Testcontainers\Modules\MySQLContainer instead. + * TODO: Remove in next major release. + */ +class MySQLContainer extends \Testcontainers\Modules\MySQLContainer { - public function __construct(string $version = 'latest', string $mysqlRootPassword = 'root') - { - parent::__construct('mysql:' . $version); - $this->withExposedPorts(3306); - $this->withEnvironment('MYSQL_ROOT_PASSWORD', $mysqlRootPassword); - $this->withWait(new WaitForLog('ready for connections')); - } - /** - * @deprecated Use constructor instead - * Left for backward compatibility - */ - public static function make(string $version = 'latest', string $mysqlRootPassword = 'root'): self - { - return new self($version, $mysqlRootPassword); - } - - public function withMySQLUser(string $username, string $password): self - { - $this->withEnvironment('MYSQL_USER', $username); - $this->withEnvironment('MYSQL_PASSWORD', $password); - - return $this; - } - - public function withMySQLDatabase(string $database): self - { - $this->withEnvironment('MYSQL_DATABASE', $database); - - return $this; - } -} +} \ No newline at end of file diff --git a/src/Container/OpenSearchContainer.php b/src/Container/OpenSearchContainer.php index a6d2b84..90e1f17 100644 --- a/src/Container/OpenSearchContainer.php +++ b/src/Container/OpenSearchContainer.php @@ -4,45 +4,12 @@ declare(strict_types=1); namespace Testcontainers\Container; -use Testcontainers\Wait\WaitForHttp; -use Testcontainers\Wait\WaitForLog; - -class OpenSearchContainer extends GenericContainer +/** + * Left for namespace backward compatibility + * @deprecated Use \Testcontainers\Modules\OpenSearchContainer instead. + * TODO: Remove in next major release. + */ +class OpenSearchContainer extends \Testcontainers\Modules\OpenSearchContainer { - public function __construct(string $version = 'latest') - { - parent::__construct('opensearchproject/opensearch:' . $version); - $this->withExposedPorts(9200); - $this->withEnvironment('discovery.type', 'single-node'); - $this->withEnvironment('OPENSEARCH_INITIAL_ADMIN_PASSWORD', 'c3o_ZPHo!'); - $this->withWait(new WaitForLog( - '/\]\s+started\?\[/', - true, - 30000 - )); - } - /** - * @deprecated Use constructor instead - * Left for backward compatibility - */ - public static function make(string $version = 'latest'): self - { - return new self($version); - } - - public function withDisabledSecurityPlugin(): self - { - $this->withEnvironment('plugins.security.disabled', 'true'); - - return $this; - } - - /** - * @deprecated Use withDisabledSecurityPlugin instead - */ - public function disableSecurityPlugin(): self - { - return $this->withDisabledSecurityPlugin(); - } -} +} \ No newline at end of file diff --git a/src/Container/PostgresContainer.php b/src/Container/PostgresContainer.php index 705f3b6..6e6e3e1 100644 --- a/src/Container/PostgresContainer.php +++ b/src/Container/PostgresContainer.php @@ -4,47 +4,12 @@ declare(strict_types=1); namespace Testcontainers\Container; -use Testcontainers\Wait\WaitForExec; - -class PostgresContainer extends GenericContainer +/** + * Left for namespace backward compatibility + * @deprecated Use \Testcontainers\Modules\PostgresContainer instead. + * TODO: Remove in next major release. + */ +class PostgresContainer extends \Testcontainers\Modules\PostgresContainer { - public function __construct( - string $version = 'latest', - public readonly string $username = 'test', - public readonly string $password = 'test', - public readonly string $database = 'test' - ) { - parent::__construct('postgres:' . $version); - $this->withExposedPorts(5432); - $this->withEnvironment('POSTGRES_USER', $this->username); - $this->withEnvironment('POSTGRES_PASSWORD', $this->password); - $this->withEnvironment('POSTGRES_DB', $this->database); - $this->withWait(new WaitForExec(["pg_isready", "-h", "127.0.0.1", "-U", $this->username])); - } - /** - * @deprecated Use constructor instead - * Left for backward compatibility - */ - public static function make(string $version = 'latest', string $dbPassword = 'root'): self - { - return new self( - version: $version, - password: $dbPassword - ); - } - - public function withPostgresUser(string $username): self - { - $this->withEnvironment('POSTGRES_USER', $username); - - return $this; - } - - public function withPostgresDatabase(string $database): self - { - $this->withEnvironment('POSTGRES_DB', $database); - - return $this; - } -} +} \ No newline at end of file diff --git a/src/Container/RedisContainer.php b/src/Container/RedisContainer.php index d568dd8..aa2f0da 100644 --- a/src/Container/RedisContainer.php +++ b/src/Container/RedisContainer.php @@ -4,23 +4,12 @@ declare(strict_types=1); namespace Testcontainers\Container; -use Testcontainers\Wait\WaitForLog; - -class RedisContainer extends GenericContainer +/** + * Left for namespace backward compatibility + * @deprecated Use \Testcontainers\Modules\RedisContainer instead. + * TODO: Remove in next major release. + */ +class RedisContainer extends \Testcontainers\Modules\RedisContainer { - public function __construct(string $version = 'latest') - { - parent::__construct('redis:' . $version); - $this->withExposedPorts(6379); - $this->withWait(new WaitForLog('Ready to accept connections')); - } - /** - * @deprecated Use constructor instead - * Left for backward compatibility - */ - public static function make(string $version = 'latest'): self - { - return new self($version); - } -} +} \ No newline at end of file diff --git a/src/Container/StartedGenericContainer.php b/src/Container/StartedGenericContainer.php new file mode 100644 index 0000000..5760eab --- /dev/null +++ b/src/Container/StartedGenericContainer.php @@ -0,0 +1,152 @@ +dockerClient = DockerContainerClient::getDockerClient(); + } + + + public function getId(): string + { + return $this->id; + } + + public function getLastExecId(): ?string + { + return $this->lastExecId; + } + + public function getClient(): Docker + { + return $this->dockerClient; + } + + /** + * @param list $command + */ + public function exec(array $command): string + { + $execConfig = (new ContainersIdExecPostBody()) + ->setCmd($command) + ->setAttachStdout(true) + ->setAttachStderr(true); + + // Create and start the exec command + /** @var IdResponse | null $exec */ + $exec = $this->dockerClient->containerExec($this->id, $execConfig); + + if($exec === null || $exec->getId() === null) { + throw new \RuntimeException('Failed to create exec command'); + } + + $this->lastExecId = $exec->getId(); + + $contents = $this->dockerClient + ->execStart($this->lastExecId, null, Client::FETCH_RESPONSE) + ?->getBody() + ->getContents() ?? ''; + + return preg_replace('/[\x00-\x1F\x7F]/u', '', $contents) ?? ''; + } + + public function stop(): StoppedTestContainer + { + $this->dockerClient->containerStop($this->id); + $this->dockerClient->containerDelete($this->id); + + return new StoppedGenericContainer($this->id); + } + + public function restart(): self + { + $this->dockerClient->containerRestart($this->id); + + return $this; + } + + public function logs(): string + { + $output = $this->dockerClient + ->containerLogs( + $this->id, + ['stdout' => true, 'stderr' => true], + DockerRuntimeClient::FETCH_RESPONSE + ) + ?->getBody() + ->getContents() ?? ''; + + return preg_replace('/[\x00-\x1F\x7F]/u', '', mb_convert_encoding($output, 'UTF-8', 'UTF-8')) ?? ''; + } + + //TODO: replace with the proper implementation + public function getHost(): string + { + return '127.0.0.1'; + } + + //TODO: not ready yet + public function getMappedPort(int $port): int + { + return $this->inspect()->ports[$port]; + } + + //TODO: not ready yet + public function getFirstMappedPort(): int + { + $containerInspectResponse = $this->dockerClient->containerInspect($this->id); + $settings = $containerInspectResponse->getNetworkSettings(); + + $ports = (array)$settings->getPorts(); + $port = array_key_first($ports); + + return (int) $ports[$port][0]->getHostPort(); + } + + public function getName(): string + { + // TODO: Implement getName() method. + return ''; + } + + public function getLabels(): array + { + // TODO: Implement getLabels() method. + return []; + } + + + public function getNetworkNames(): array + { + // TODO: Implement getNetworkNames() method. + return []; + } + + public function getNetworkId(string $networkName): string + { + // TODO: Implement getNetworkId() method. + return ''; + } + + public function getIpAddress(string $networkName): string + { + // TODO: Implement getIpAddress() method. + return ''; + } +} diff --git a/src/Container/StartedTestContainer.php b/src/Container/StartedTestContainer.php new file mode 100644 index 0000000..8eefcdf --- /dev/null +++ b/src/Container/StartedTestContainer.php @@ -0,0 +1,43 @@ + $command + */ + public function exec(array $command): string; + + public function logs(): string; +} diff --git a/src/Container/StoppedGenericContainer.php b/src/Container/StoppedGenericContainer.php new file mode 100644 index 0000000..f226c8c --- /dev/null +++ b/src/Container/StoppedGenericContainer.php @@ -0,0 +1,17 @@ +id; + } +} diff --git a/src/Container/StoppedTestContainer.php b/src/Container/StoppedTestContainer.php new file mode 100644 index 0000000..423c230 --- /dev/null +++ b/src/Container/StoppedTestContainer.php @@ -0,0 +1,10 @@ +|string $env + */ + public function withEnvironment(array | string $env, ?string $value): static; + + /** + * @param array $command + */ + public function withCommand(array $command): static; + + public function withEntrypoint(string $entryPoint): static; + + /** @param int|string|array $ports One or more ports to expose. */ + public function withExposedPorts(...$ports): static; + + public function withWait(WaitStrategy $waitStrategy): static; + + public function withNetwork(string $networkName): static; + + public function withPrivilegedMode(): static; +} diff --git a/src/ContainerRuntime/ContainerRuntimeClient.php b/src/ContainerClient/DockerContainerClient.php similarity index 93% rename from src/ContainerRuntime/ContainerRuntimeClient.php rename to src/ContainerClient/DockerContainerClient.php index 5728217..84bf645 100644 --- a/src/ContainerRuntime/ContainerRuntimeClient.php +++ b/src/ContainerClient/DockerContainerClient.php @@ -1,10 +1,10 @@ withExposedPorts(3306); + $this->withWait(new WaitForLog('ready for connections')); + $this->withEnvironment('MARIADB_ROOT_PASSWORD', $mysqlRootPassword); + } + + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest', string $mysqlRootPassword = 'root'): self + { + return new self($version, $mysqlRootPassword); + } + + public function withMariaDBUser(string $username, string $password): self + { + $this->withEnvironment('MARIADB_USER', $username); + $this->withEnvironment('MARIADB_PASSWORD', $password); + + return $this; + } + + public function withMariaDBDatabase(string $database): self + { + $this->withEnvironment('MARIADB_DATABASE', $database); + + return $this; + } +} diff --git a/src/Modules/MySQLContainer.php b/src/Modules/MySQLContainer.php new file mode 100644 index 0000000..fcef5bb --- /dev/null +++ b/src/Modules/MySQLContainer.php @@ -0,0 +1,43 @@ +withExposedPorts(3306); + $this->withEnvironment('MYSQL_ROOT_PASSWORD', $mysqlRootPassword); + $this->withWait(new WaitForLog('ready for connections')); + } + + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest', string $mysqlRootPassword = 'root'): self + { + return new self($version, $mysqlRootPassword); + } + + public function withMySQLUser(string $username, string $password): self + { + $this->withEnvironment('MYSQL_USER', $username); + $this->withEnvironment('MYSQL_PASSWORD', $password); + + return $this; + } + + public function withMySQLDatabase(string $database): self + { + $this->withEnvironment('MYSQL_DATABASE', $database); + + return $this; + } +} diff --git a/src/Modules/OpenSearchContainer.php b/src/Modules/OpenSearchContainer.php new file mode 100644 index 0000000..24d3f7d --- /dev/null +++ b/src/Modules/OpenSearchContainer.php @@ -0,0 +1,48 @@ +withExposedPorts(9200); + $this->withEnvironment('discovery.type', 'single-node'); + $this->withEnvironment('OPENSEARCH_INITIAL_ADMIN_PASSWORD', 'c3o_ZPHo!'); + $this->withWait(new WaitForLog( + '/\]\s+started\?\[/', + true, + 30000 + )); + } + + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest'): self + { + return new self($version); + } + + public function withDisabledSecurityPlugin(): self + { + $this->withEnvironment('plugins.security.disabled', 'true'); + + return $this; + } + + /** + * @deprecated Use withDisabledSecurityPlugin instead + */ + public function disableSecurityPlugin(): self + { + return $this->withDisabledSecurityPlugin(); + } +} diff --git a/src/Modules/PostgresContainer.php b/src/Modules/PostgresContainer.php new file mode 100644 index 0000000..8c30903 --- /dev/null +++ b/src/Modules/PostgresContainer.php @@ -0,0 +1,51 @@ +withExposedPorts(5432); + $this->withEnvironment('POSTGRES_USER', $this->username); + $this->withEnvironment('POSTGRES_PASSWORD', $this->password); + $this->withEnvironment('POSTGRES_DB', $this->database); + $this->withWait(new WaitForExec(["pg_isready", "-h", "127.0.0.1", "-U", $this->username])); + } + + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest', string $dbPassword = 'root'): self + { + return new self( + version: $version, + password: $dbPassword + ); + } + + public function withPostgresUser(string $username): self + { + $this->withEnvironment('POSTGRES_USER', $username); + + return $this; + } + + public function withPostgresDatabase(string $database): self + { + $this->withEnvironment('POSTGRES_DB', $database); + + return $this; + } +} diff --git a/src/Modules/RedisContainer.php b/src/Modules/RedisContainer.php new file mode 100644 index 0000000..a31dde9 --- /dev/null +++ b/src/Modules/RedisContainer.php @@ -0,0 +1,27 @@ +withExposedPorts(6379); + $this->withWait(new WaitForLog('Ready to accept connections')); + } + + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest'): self + { + return new self($version); + } +} diff --git a/src/Utils/PortGenerator/FixedPortGenerator.php b/src/Utils/PortGenerator/FixedPortGenerator.php new file mode 100644 index 0000000..9afe0b7 --- /dev/null +++ b/src/Utils/PortGenerator/FixedPortGenerator.php @@ -0,0 +1,25 @@ +ports[$this->portIndex])) { + throw new \RuntimeException('No more ports available in the fixed list.'); + } + + return $this->ports[$this->portIndex++]; + } +} diff --git a/src/Utils/PortGenerator/PortGenerator.php b/src/Utils/PortGenerator/PortGenerator.php new file mode 100644 index 0000000..df7da1a --- /dev/null +++ b/src/Utils/PortGenerator/PortGenerator.php @@ -0,0 +1,10 @@ +getRandomPort($this->randomBetweenInclusive(10000, 65535)); + } + + private function randomBetweenInclusive(int $min, int $max): int + { + return random_int($min, $max); + } + + private function getRandomPort(int $port): int + { + $connection = @fsockopen("localhost", $port); + + if (is_resource($connection)) { + fclose($connection); + throw new \RuntimeException("Port $port is already in use."); + } + + return $port; + } +} \ No newline at end of file diff --git a/src/Utils/PortGenerator/RandomUniquePortGenerator.php b/src/Utils/PortGenerator/RandomUniquePortGenerator.php new file mode 100644 index 0000000..f405e25 --- /dev/null +++ b/src/Utils/PortGenerator/RandomUniquePortGenerator.php @@ -0,0 +1,26 @@ +portGenerator->generatePort(); + } while (in_array($port, self::$assignedPorts)); + + self::$assignedPorts[] = $port; + + return $port; + } +} diff --git a/src/Wait/BaseWait.php b/src/Wait/BaseWait.php deleted file mode 100644 index b9b607a..0000000 --- a/src/Wait/BaseWait.php +++ /dev/null @@ -1,20 +0,0 @@ -dockerClient = ContainerRuntimeClient::getDockerClient(); - } - - abstract public function wait(string $id): void; -} diff --git a/src/Wait/BaseWaitStrategy.php b/src/Wait/BaseWaitStrategy.php new file mode 100644 index 0000000..432aedf --- /dev/null +++ b/src/Wait/BaseWaitStrategy.php @@ -0,0 +1,17 @@ +getId(); $startTime = microtime(true) * 1000; while (true) { @@ -25,7 +27,7 @@ class WaitForContainerRunning extends BaseWait } /** @var ContainersIdJsonGetResponse200 | null $containerInspect */ - $containerInspect = $this->dockerClient->containerInspect($id); + $containerInspect = $container->getClient()->containerInspect($id); $containerStatus = $containerInspect?->getState()?->getStatus(); if ($containerStatus === 'running') { diff --git a/src/Wait/WaitForExec.php b/src/Wait/WaitForExec.php index 6d8aea9..044471c 100644 --- a/src/Wait/WaitForExec.php +++ b/src/Wait/WaitForExec.php @@ -5,14 +5,15 @@ declare(strict_types=1); namespace Testcontainers\Wait; use Closure; -use Docker\API\Client; use Docker\API\Model\ContainersIdExecPostBody; +use Docker\API\Model\ExecIdJsonGetResponse200; +use Testcontainers\Container\StartedTestContainer; use Testcontainers\Exception\ContainerWaitingTimeoutException; /** * Uses $timout and $pollInterval in milliseconds to set the parameters for waiting. */ -class WaitForExec extends BaseWait +class WaitForExec extends BaseWaitStrategy { protected ContainersIdExecPostBody $execConfig; @@ -28,32 +29,23 @@ class WaitForExec extends BaseWait parent::__construct($timeout, $pollInterval); } - public function wait(string $id): void + public function wait(StartedTestContainer $container): void { - $this->execConfig = (new ContainersIdExecPostBody()) - ->setCmd($this->command) - ->setAttachStdout(true) - ->setAttachStderr(true); - $startTime = microtime(true) * 1000; while (true) { $elapsedTime = (microtime(true) * 1000) - $startTime; if ($elapsedTime > $this->timeout) { - throw new ContainerWaitingTimeoutException($id); + throw new ContainerWaitingTimeoutException($container->getId()); } - // Create and start the exec command - $exec = $this->dockerClient->containerExec($id, $this->execConfig); - $contents = $this->dockerClient - ->execStart($exec->getId(), null, Client::FETCH_RESPONSE) - ?->getBody() - ->getContents() ?? ''; + $contents = $container->exec($this->command); // Inspect the exec to check the exit code - $execInspect = $this->dockerClient->execInspect($exec->getId()); - $exitCode = $execInspect->getExitCode(); + /** @var ExecIdJsonGetResponse200 | null $execInspect */ + $execInspect = $container->getClient()->execInspect($container->getLastExecId() ?? ''); + $exitCode = $execInspect?->getExitCode(); // If a custom check function is provided, use it to validate the command output if ($this->checkFunction !== null) { diff --git a/src/Wait/WaitForHealthCheck.php b/src/Wait/WaitForHealthCheck.php index 995b99d..4a0c9c8 100644 --- a/src/Wait/WaitForHealthCheck.php +++ b/src/Wait/WaitForHealthCheck.php @@ -5,25 +5,18 @@ declare(strict_types=1); namespace Testcontainers\Wait; use Docker\Docker; -use Docker\DockerClientFactory; use Http\Client\Socket\Exception\TimeoutException; -use Testcontainers\ContainerRuntime\ContainerRuntimeClient; +use Testcontainers\Container\StartedTestContainer; use Testcontainers\Exception\ContainerNotReadyException; -class WaitForHealthCheck implements WaitInterface +class WaitForHealthCheck extends BaseWaitStrategy { - protected Docker $dockerClient; - protected int $timeout; - protected int $pollInterval; - - public function __construct(int $timeout = 5000, int $pollInterval = 1000) + public function __construct(protected int $timeout = 5000, protected int $pollInterval = 1000) { - $this->dockerClient = ContainerRuntimeClient::getDockerClient(); - $this->timeout = $timeout; - $this->pollInterval = $pollInterval; + parent::__construct($timeout, $pollInterval); } - public function wait(string $id): void + public function wait(StartedTestContainer $container): void { $startTime = microtime(true) * 1000; @@ -34,10 +27,11 @@ class WaitForHealthCheck implements WaitInterface throw new TimeoutException(sprintf("Health check not healthy after %d ms", $this->timeout)); } - $containerInspect = $this->dockerClient->containerInspect($id, [], Docker::FETCH_RESPONSE); + /** @var \Psr\Http\Message\ResponseInterface | null $containerInspect */ + $containerInspect = $container->getClient()->containerInspect($container->getId(), [], Docker::FETCH_RESPONSE); //$containerStatus = $containerInspect?->getArrayCopy() ?? null; var_dump($containerInspect->getBody()->getContents()); - $containerStatus=''; + $containerStatus = ''; if ($containerStatus === 'healthy') { return; } diff --git a/src/Wait/WaitForHttp.php b/src/Wait/WaitForHttp.php index f8927e9..906dc8b 100644 --- a/src/Wait/WaitForHttp.php +++ b/src/Wait/WaitForHttp.php @@ -7,7 +7,7 @@ namespace Testcontainers\Wait; use Docker\Docker; use Testcontainers\Exception\ContainerNotReadyException; -class WaitForHttp implements WaitInterface +class WaitForHttp implements WaitStrategy { public const METHOD_GET = 'GET'; public const METHOD_POST = 'POST'; diff --git a/src/Wait/WaitForLog.php b/src/Wait/WaitForLog.php index c8ba18c..a7ff614 100644 --- a/src/Wait/WaitForLog.php +++ b/src/Wait/WaitForLog.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace Testcontainers\Wait; -use Docker\API\Runtime\Client\Client; +use Testcontainers\Container\StartedTestContainer; use Testcontainers\Exception\ContainerWaitingTimeoutException; /** * Uses $timout and $pollInterval in milliseconds to set the parameters for waiting. */ -class WaitForLog extends BaseWait +class WaitForLog extends BaseWaitStrategy { public function __construct( protected string $message, @@ -21,7 +21,7 @@ class WaitForLog extends BaseWait parent::__construct($timeout, $pollInterval); } - public function wait(string $id): void + public function wait(StartedTestContainer $container): void { $startTime = microtime(true) * 1000; @@ -29,15 +29,10 @@ class WaitForLog extends BaseWait $elapsedTime = (microtime(true) * 1000) - $startTime; if ($elapsedTime > $this->timeout) { - throw new ContainerWaitingTimeoutException($id); + throw new ContainerWaitingTimeoutException($container->getId()); } - $output = $this->dockerClient - ->containerLogs($id, ['stdout' => true, 'stderr' => true], Client::FETCH_RESPONSE) - ?->getBody() - ->getContents() ?? ''; - - $output = preg_replace('/[\x00-\x1F\x7F]/u', '', mb_convert_encoding($output, 'UTF-8', 'UTF-8')) ?? ''; + $output = $container->logs(); if ($this->enableRegex) { if (preg_match($this->message, $output)) { diff --git a/src/Wait/WaitForTcpPortOpen.php b/src/Wait/WaitForTcpPortOpen.php index 6fea945..e6c3700 100644 --- a/src/Wait/WaitForTcpPortOpen.php +++ b/src/Wait/WaitForTcpPortOpen.php @@ -9,7 +9,7 @@ use JsonException; use RuntimeException; use Testcontainers\Exception\ContainerNotReadyException; -final class WaitForTcpPortOpen implements WaitInterface +final class WaitForTcpPortOpen implements WaitStrategy { private Docker $dockerClient; diff --git a/src/Wait/WaitInterface.php b/src/Wait/WaitInterface.php deleted file mode 100644 index 4e75ce5..0000000 --- a/src/Wait/WaitInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -remove(); + self::$container->stop(); } } diff --git a/tests/Integration/MariaDBContainerTest.php b/tests/Integration/MariaDBContainerTest.php index f1da8c8..b2ad631 100644 --- a/tests/Integration/MariaDBContainerTest.php +++ b/tests/Integration/MariaDBContainerTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Testcontainers\Tests\Integration; -use Testcontainers\Container\MariaDBContainer; +use Testcontainers\Modules\MariaDBContainer; class MariaDBContainerTest extends ContainerTestCase { @@ -19,7 +19,11 @@ class MariaDBContainerTest extends ContainerTestCase public function testMariaDBContainer(): void { $pdo = new \PDO( - sprintf('mysql:host=%s;port=3306', self::$container->getAddress()), + sprintf( + 'mysql:host=%s;port=%d', + self::$container->getHost(), + self::$container->getFirstMappedPort() + ), 'bar', 'baz', ); diff --git a/tests/Integration/MySQLContainerTest.php b/tests/Integration/MySQLContainerTest.php index 51c98e3..c88f911 100644 --- a/tests/Integration/MySQLContainerTest.php +++ b/tests/Integration/MySQLContainerTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Testcontainers\Tests\Integration; -use Testcontainers\Container\MySQLContainer; +use Testcontainers\Modules\MySQLContainer; class MySQLContainerTest extends ContainerTestCase { @@ -19,7 +19,11 @@ class MySQLContainerTest extends ContainerTestCase public function testMySQLContainer(): void { $pdo = new \PDO( - sprintf('mysql:host=%s;port=3306', '127.0.0.1'), + sprintf( + 'mysql:host=%s;port=%d', + self::$container->getHost(), + self::$container->getFirstMappedPort() + ), 'bar', 'baz', ); diff --git a/tests/Integration/OldTests/ContainerTest.php b/tests/Integration/OldTests/ContainerTest.php new file mode 100644 index 0000000..f13f163 --- /dev/null +++ b/tests/Integration/OldTests/ContainerTest.php @@ -0,0 +1,139 @@ +withMySQLDatabase('foo'); + $container->withMySQLUser('bar', 'baz'); + + $container->run(); + + $pdo = new \PDO( + sprintf('mysql:host=%s;port=3306', $container->getAddress()), + 'bar', + 'baz', + ); + + $query = $pdo->query('SHOW databases'); + + $this->assertInstanceOf(\PDOStatement::class, $query); + + $databases = $query->fetchAll(\PDO::FETCH_COLUMN); + + $this->assertContains('foo', $databases); + + $container->stop(); + } + + public function testMariaDB(): void + { + $container = MariaDBContainer::make(); + $container->withMariaDBDatabase('foo'); + $container->withMariaDBUser('bar', 'baz'); + + $container->run(); + + $pdo = new \PDO( + sprintf('mysql:host=%s;port=3306', $container->getAddress()), + 'bar', + 'baz', + ); + + $query = $pdo->query('SHOW databases'); + + $this->assertInstanceOf(\PDOStatement::class, $query); + + $databases = $query->fetchAll(\PDO::FETCH_COLUMN); + + $this->assertContains('foo', $databases); + + $container->stop(); + } + + public function testRedis(): void + { + $container = RedisContainer::make(); + + $container->run(); + + $redis = new Client([ + 'scheme' => 'tcp', + 'host' => $container->getAddress(), + 'port' => 6379, + ]); + + $redis->ping(); + + $this->assertTrue($redis->isConnected()); + + $container->stop(); + } + + /** + * @throws \JsonException + */ + public function testOpenSearch(): void + { + $container = OpenSearchContainer::make(); + $container->disableSecurityPlugin(); + + $container->run(); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 9200)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = (string) curl_exec($ch); + + $this->assertNotEmpty($response); + + /** @var array{cluster_name: string} $data */ + $data = json_decode($response, true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR); + + $this->assertArrayHasKey('cluster_name', $data); + + $this->assertEquals('docker-cluster', $data['cluster_name']); + } + + public function testPostgreSQLContainer(): void + { + $container = PostgresContainer::make('latest', 'test') + ->withPostgresUser('test') + ->withPostgresDatabase('foo') + ->run(); + + + $pdo = new \PDO( + sprintf('pgsql:host=%s;port=5432;dbname=foo', $container->getAddress()), + 'test', + 'test', + ); + + $query = $pdo->query('SELECT datname FROM pg_database'); + + $this->assertInstanceOf(\PDOStatement::class, $query); + + $databases = $query->fetchAll(\PDO::FETCH_COLUMN); + + $this->assertContains('foo', $databases); + + $container->stop(); + } +} diff --git a/tests/Integration/OldTests/WaitStrategyTest.php b/tests/Integration/OldTests/WaitStrategyTest.php new file mode 100644 index 0000000..fab22a2 --- /dev/null +++ b/tests/Integration/OldTests/WaitStrategyTest.php @@ -0,0 +1,151 @@ +withEnvironment('MYSQL_ROOT_PASSWORD', 'root') + ->withWait( + new WaitForExec([ + 'mysqladmin', 'ping', + '-h', '127.0.0.1', + ]) + ); + + $container->run(); + + $pdo = new \PDO( + sprintf('mysql:host=%s;port=3306', $container->getAddress()), + 'root', + 'root' + ); + + $query = $pdo->query('select version()'); + + $this->assertInstanceOf(\PDOStatement::class, $query); + + $version = $query->fetchColumn(); + + $this->assertNotEmpty($version); + } + + // public function testWaitForLog(): void + // { + // $container = GenericContainer::make('redis:6.2.5') + // ->withWait(new WaitForLog('Ready to accept connections')); + // + // $container->run(); + // + // $redis = new Client([ + // 'scheme' => 'tcp', + // 'host' => $container->getAddress(), + // 'port' => 6379, + // ]); + // + // $redis->set('foo', 'bar'); + // + // $this->assertEquals('bar', $redis->get('foo')); + // + // $container->stop(); + // + // $this->expectException(ConnectionException::class); + // + // $redis->get('foo'); + // + // $container->remove(); + // } + // + // public function testWaitForHTTP(): void + // { + // $container = GenericContainer::make('nginx:alpine') + // ->withWait(WaitForHttp::make(80)); + // + // $container->run(); + // + // $ch = curl_init(); + // curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); + // curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // + // $response = (string) curl_exec($ch); + // + // curl_close($ch); + // + // $this->assertNotEmpty($response); + // } + // + // /** + // * @dataProvider provideWaitForTcpPortOpen + // */ + // public function testWaitForTcpPortOpen(bool $wait): void + // { + // $container = GenericContainer::make('nginx:alpine'); + // + // if ($wait) { + // $container->withWait(WaitForTcpPortOpen::make(80)); + // } + // + // $container->run(); + // + // if ($wait) { + // static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); + // return; + // } + // + // $containerId = $container->getId(); + // + // $this->expectExceptionObject(new ContainerNotReadyException($containerId)); + // + // (new WaitForTcpPortOpen(8080))->wait($containerId); + // } + // + // /** + // * @return array> + // */ + // public function provideWaitForTcpPortOpen(): array + // { + // return [ + // 'Can connect to container' => [true], + // 'Cannot connect to container' => [false], + // ]; + // } + // + // public function testWaitForHealthCheck(): void + // { + // $container = GenericContainer::make('nginx') + // ->withHealthCheckCommand('curl --fail http://localhost') + // ->withWait(new WaitForHealthCheck()); + // + // $container->run(); + // + // $ch = curl_init(); + // + // curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); + // curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // + // $response = curl_exec($ch); + // + // $this->assertNotEmpty($response); + // $this->assertIsString($response); + // + // $this->assertStringContainsString('Welcome to nginx!', $response); + // } +} diff --git a/tests/Integration/OpenSearchContainerTest.php b/tests/Integration/OpenSearchContainerTest.php index 8e8db0d..19c34aa 100644 --- a/tests/Integration/OpenSearchContainerTest.php +++ b/tests/Integration/OpenSearchContainerTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Testcontainers\Tests\Integration; -use Testcontainers\Container\OpenSearchContainer; +use Testcontainers\Modules\OpenSearchContainer; class OpenSearchContainerTest extends ContainerTestCase { @@ -21,7 +21,11 @@ class OpenSearchContainerTest extends ContainerTestCase public function testOpenSearch(): void { $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', '127.0.0.1', 9200)); + curl_setopt($ch, CURLOPT_URL, sprintf( + 'http://%s:%d', + self::$container->getHost(), + self::$container->getFirstMappedPort() + )); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = (string) curl_exec($ch); diff --git a/tests/Integration/PostgreSQLContainerTest.php b/tests/Integration/PostgreSQLContainerTest.php index 2b6bd70..e1f81c5 100644 --- a/tests/Integration/PostgreSQLContainerTest.php +++ b/tests/Integration/PostgreSQLContainerTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Testcontainers\Tests\Integration; -use Testcontainers\Container\PostgresContainer; +use Testcontainers\Modules\PostgresContainer; class PostgreSQLContainerTest extends ContainerTestCase { @@ -19,7 +19,11 @@ class PostgreSQLContainerTest extends ContainerTestCase public function testPostgreSQLContainer(): void { $pdo = new \PDO( - 'pgsql:host=127.0.0.1;port=5432;dbname=foo', + sprintf( + 'pgsql:host=%s;port=%d;dbname=foo', + self::$container->getHost(), + self::$container->getFirstMappedPort() + ), 'bar', 'test', ); diff --git a/tests/Integration/RedisContainerTest.php b/tests/Integration/RedisContainerTest.php index 08dec78..0379dce 100644 --- a/tests/Integration/RedisContainerTest.php +++ b/tests/Integration/RedisContainerTest.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Testcontainers\Tests\Integration; use Predis\Client; -use Testcontainers\Container\RedisContainer; +use Testcontainers\Modules\RedisContainer; class RedisContainerTest extends ContainerTestCase { @@ -18,8 +18,8 @@ class RedisContainerTest extends ContainerTestCase public function testRedisContainer(): void { $redisClient = new Client([ - 'host' => 'localhost', - 'port' => 6379, + 'host' => self::$container->getHost(), + 'port' => self::$container->getFirstMappedPort(), ]); $redisClient->ping(); diff --git a/tests/Integration/WaitStrategyTest.php b/tests/Integration/WaitStrategyTest.php deleted file mode 100644 index 7bdbd3d..0000000 --- a/tests/Integration/WaitStrategyTest.php +++ /dev/null @@ -1,157 +0,0 @@ -withEnvironment('MYSQL_ROOT_PASSWORD', 'root') - ->withWait(new WaitForExec(['mysqladmin', 'ping', '-h', '127.0.0.1'], function (Process $process) use (&$called) { - $called = true; - })); - - $container->run(); - - $this->assertTrue($called, 'Wait function was not called'); - unset($called); - - $pdo = new \PDO( - sprintf('mysql:host=%s;port=3306', $container->getAddress()), - 'root', - 'root' - ); - - $query = $pdo->query('select version()'); - - $this->assertInstanceOf(\PDOStatement::class, $query); - - $version = $query->fetchColumn(); - - $this->assertNotEmpty($version); - } - - public function testWaitForLog(): void - { - $container = GenericContainer::make('redis:6.2.5') - ->withWait(new WaitForLog('Ready to accept connections')); - - $container->run(); - - $redis = new Client([ - 'scheme' => 'tcp', - 'host' => $container->getAddress(), - 'port' => 6379, - ]); - - $redis->set('foo', 'bar'); - - $this->assertEquals('bar', $redis->get('foo')); - - $container->stop(); - - $this->expectException(ConnectionException::class); - - $redis->get('foo'); - - $container->remove(); - } - - public function testWaitForHTTP(): void - { - $container = GenericContainer::make('nginx:alpine') - ->withWait(WaitForHttp::make(80)); - - $container->run(); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - $response = (string) curl_exec($ch); - - curl_close($ch); - - $this->assertNotEmpty($response); - } - - /** - * @dataProvider provideWaitForTcpPortOpen - */ - public function testWaitForTcpPortOpen(bool $wait): void - { - $container = GenericContainer::make('nginx:alpine'); - - if ($wait) { - $container->withWait(WaitForTcpPortOpen::make(80)); - } - - $container->run(); - - if ($wait) { - static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); - return; - } - - $containerId = $container->getId(); - - $this->expectExceptionObject(new ContainerNotReadyException($containerId)); - - (new WaitForTcpPortOpen(8080))->wait($containerId); - } - - /** - * @return array> - */ - public function provideWaitForTcpPortOpen(): array - { - return [ - 'Can connect to container' => [true], - 'Cannot connect to container' => [false], - ]; - } - - public function testWaitForHealthCheck(): void - { - $container = GenericContainer::make('nginx') - ->withHealthCheckCommand('curl --fail http://localhost') - ->withWait(new WaitForHealthCheck()); - - $container->run(); - - $ch = curl_init(); - - curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - $response = curl_exec($ch); - - $this->assertNotEmpty($response); - $this->assertIsString($response); - - $this->assertStringContainsString('Welcome to nginx!', $response); - } -}