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);
- }
-}