- Move most of the stuff for backwards compatibility support into one class

- Added random port logic
- Updated wait wtrategies
- API alignments to make it similar to other official implementations
- ...
This commit is contained in:
Sergei Shitikov
2024-09-06 19:08:37 +02:00
parent a956794279
commit f05ea0020d
43 changed files with 1191 additions and 601 deletions
+7 -7
View File
@@ -19,7 +19,7 @@ composer req --dev testcontainers/testcontainers
use Testcontainers\Container\GenericContainer; use Testcontainers\Container\GenericContainer;
$container = GenericContainer::make('nginx:alpine'); $container = new GenericContainer::make('nginx:alpine');
// set an environment variable // set an environment variable
$container->withEnvironment('name', 'var'); $container->withEnvironment('name', 'var');
@@ -58,7 +58,7 @@ $container->withWait(new WaitForHealthCheck());
```php ```php
<?php <?php
use Testcontainers\Container\MySQLContainer; use Testcontainers\Modules\MySQLContainer;
$container = MySQLContainer::make('8.0'); $container = MySQLContainer::make('8.0');
$container->withMySQLDatabase('foo'); $container->withMySQLDatabase('foo');
@@ -80,7 +80,7 @@ $pdo = new \PDO(
```php ```php
<?php <?php
use Testcontainers\Container\MariaDBContainer; use Testcontainers\Modules\MariaDBContainer;
$container = MariaDBContainer::make('8.0'); $container = MariaDBContainer::make('8.0');
$container->withMariaDBDatabase('foo'); $container->withMariaDBDatabase('foo');
@@ -102,7 +102,7 @@ $pdo = new \PDO(
```php ```php
<?php <?php
use Testcontainers\Container\PostgresContainer; use Testcontainers\Modules\PostgresContainer;
$container = PostgresContainer::make('15.0', 'password'); $container = PostgresContainer::make('15.0', 'password');
$container->withPostgresDatabase('database'); $container->withPostgresDatabase('database');
@@ -123,7 +123,7 @@ $pdo = new \PDO(
```php ```php
use Testcontainers\Container\RedisContainer; use Testcontainers\Modules\RedisContainer;
$container = RedisContainer::make('6.0'); $container = RedisContainer::make('6.0');
@@ -139,7 +139,7 @@ $redis->connect($container->getAddress());
```php ```php
use Testcontainers\Container\OpenSearchContainer; use Testcontainers\Modules\OpenSearchContainer;
$container = OpenSearchContainer::make('2'); $container = OpenSearchContainer::make('2');
$container->disableSecurityPlugin(); $container->disableSecurityPlugin();
@@ -166,7 +166,7 @@ use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
use Doctrine\Common\EventManager; use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Tools\DsnParser; use Doctrine\DBAL\Tools\DsnParser;
use Testcontainers\Container\PostgresContainer; use Testcontainers\Modules\PostgresContainer;
class TestConnectionFactory extends ConnectionFactory class TestConnectionFactory extends ConnectionFactory
{ {
-1
View File
@@ -14,7 +14,6 @@
<testsuites> <testsuites>
<testsuite name="default"> <testsuite name="default">
<directory>tests</directory> <directory>tests</directory>
<exclude>tests/Integration/WaitStrategyTest.php</exclude>
</testsuite> </testsuite>
</testsuites> </testsuites>
+147 -2
View File
@@ -6,9 +6,154 @@ namespace Testcontainers\Container;
/** /**
* Added for backward compatibility. * Added for backward compatibility.
* Just a wrapper for GenericContainer.
* @deprecated Use GenericContainer instead. * @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<string> $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;
}
} }
+63 -174
View File
@@ -4,39 +4,28 @@ declare(strict_types=1);
namespace Testcontainers\Container; namespace Testcontainers\Container;
use Docker\API\Client;
use Docker\API\Exception\ContainerCreateNotFoundException; use Docker\API\Exception\ContainerCreateNotFoundException;
use Docker\API\Model\ContainersCreatePostBody; use Docker\API\Model\ContainersCreatePostBody;
use Docker\API\Model\ContainersIdExecPostBody;
use Docker\API\Model\HealthConfig; use Docker\API\Model\HealthConfig;
use Docker\API\Model\HostConfig; use Docker\API\Model\HostConfig;
use Docker\API\Model\Mount; use Docker\API\Model\Mount;
use Docker\API\Model\PortBinding; use Docker\API\Model\PortBinding;
use Docker\Docker; use Docker\Docker;
use Psr\Http\Message\ResponseInterface; use InvalidArgumentException;
use Testcontainers\ContainerRuntime\ContainerRuntimeClient; use Testcontainers\ContainerClient\DockerContainerClient;
use Testcontainers\Wait\WaitForContainerRunning; use Testcontainers\Utils\PortGenerator\RandomUniquePortGenerator;
use Testcontainers\Wait\WaitInterface; use Testcontainers\Wait\WaitForContainer;
use Testcontainers\Wait\WaitStrategy;
/** class GenericContainer implements TestContainer
* @phpstan-type ContainerInspectSingleNetwork array<int, array{'NetworkSettings': array{'IPAddress': string}}>
* @phpstan-type ContainerInspectMultipleNetworks array<int, array{'NetworkSettings': array{'Networks': array<string, array{'IPAddress': string}>}}>
* @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
{ {
protected Docker $dockerClient; protected Docker $dockerClient;
protected ContainersCreatePostBody $containerConfig;
protected string $image; protected string $image;
protected string $containerName;
protected string $id; protected string $id;
/** @var array<string> */ /** @var list<string> */
protected array $command = []; protected array $command = [];
protected ?string $entryPoint = null; protected ?string $entryPoint = null;
@@ -48,7 +37,7 @@ class GenericContainer
*/ */
protected array $env = []; protected array $env = [];
protected WaitInterface $wait; protected WaitStrategy $waitStrategy;
protected bool $isPrivileged = false; protected bool $isPrivileged = false;
protected ?string $networkName = null; protected ?string $networkName = null;
@@ -64,16 +53,7 @@ class GenericContainer
public function __construct(string $image) public function __construct(string $image)
{ {
$this->image = $image; $this->image = $image;
$this->dockerClient = ContainerRuntimeClient::getDockerClient(); $this->dockerClient = DockerContainerClient::getDockerClient();
}
/**
* @deprecated Use constructor instead
* Left for backward compatibility
*/
public static function make(string $image): self
{
return new GenericContainer($image);
} }
public function getId(): string public function getId(): string
@@ -81,58 +61,56 @@ class GenericContainer
return $this->id; return $this->id;
} }
public function withCommand(array $command): self /**
* @param list<string> $command
*/
public function withCommand(array $command): static
{ {
$this->command = $command; $this->command = $command;
return $this; return $this;
} }
public function exec(array $command): string public function withEntryPoint(string $entryPoint): static
{
$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
{ {
$this->entryPoint = $entryPoint; $this->entryPoint = $entryPoint;
return $this; 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<string, string> $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; return $this;
} }
public function withImage(string $image): self public function withWait(WaitStrategy $waitStrategy): static
{ {
$this->image = $image; $this->waitStrategy = $waitStrategy;
return $this; return $this;
} }
public function withWait(WaitInterface $wait): self public function withHealthCheckCommand(string $command, int $healthCheckIntervalInMS = 1000): static
{
$this->wait = $wait;
return $this;
}
public function withHealthCheckCommand(string $command, int $healthCheckIntervalInMS = 1000): self
{ {
$this->healthConfig = new HealthConfig([ $this->healthConfig = new HealthConfig([
'Test' => ['CMD', $command], 'Test' => ['CMD', $command],
@@ -142,33 +120,22 @@ class GenericContainer
return $this; 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]); $this->mounts[] = new Mount(['type' => 'bind', 'source' => $localPath, 'target' => $containerPath]);
return $this; return $this;
} }
/**
* @deprecated Use `withExposedPorts` instead
*/
public function withPort(string $localPort, string $containerPort): self
{
return $this->withExposedPorts($containerPort);
}
/**
* @psalm-param string|int|array<string|int> $port
*/
/** /**
* Add ports to be exposed by the Docker container. * Add ports to be exposed by the Docker container.
* This method accepts multiple inputs: single port, multiple ports, or ports with specific protocols * This method accepts multiple inputs: single port, multiple ports, or ports with specific protocols
* to attempt to align with other language implementations. * to attempt to align with other language implementations.
* *
* @psalm-param int|string|array<int|string> $ports One or more ports to expose. * @psalm-param int|string|array<int|string> $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) { foreach ($ports as $port) {
if (is_array($port)) { if (is_array($port)) {
@@ -207,46 +174,51 @@ class GenericContainer
return $port; return $port;
} }
public function withPrivileged(bool $privileged = true): self public function withPrivilegedMode(bool $privileged = true): static
{ {
$this->isPrivileged = $privileged; $this->isPrivileged = $privileged;
return $this; return $this;
} }
public function withNetwork(string $networkName): self //TODO: not yet implemented
public function withNetwork(string $networkName): static
{ {
$this->networkName = $networkName; $this->networkName = $networkName;
return $this; return $this;
} }
public function stop(): self //TODO: needs refactoring
{ public function start(): StartedGenericContainer
$this->dockerClient->containerStop($this->id);
return $this;
}
public function start(): self
{ {
try { try {
$containerCreatePostBody = new ContainersCreatePostBody(); $containerCreatePostBody = new ContainersCreatePostBody();
//setup only if we need to expose ports //handle withExposedPorts
if(!empty($this->exposedPorts)) { if(!empty($this->exposedPorts)) {
$portGenerator = new RandomUniquePortGenerator();
$portMap = new \ArrayObject(); $portMap = new \ArrayObject();
foreach ($this->exposedPorts as $port) { foreach ($this->exposedPorts as $port) {
$portBinding = new PortBinding(); $portBinding = new PortBinding();
$portBinding->setHostPort(explode('/', $port)[0]); $portBinding->setHostPort((string) $portGenerator->generatePort());
$portBinding->setHostIp('0.0.0.0'); $portBinding->setHostIp('0.0.0.0');
$portMap[$port] = [$portBinding]; $portMap[$port] = [$portBinding];
} }
$hostConfig = new HostConfig(); $hostConfig = new HostConfig();
$hostConfig->setPortBindings($portMap); $hostConfig->setPortBindings($portMap);
//handle withPrivilegedMode
if($this->isPrivileged) {
$hostConfig->setPrivileged($this->isPrivileged);
}
$containerCreatePostBody->setHostConfig($hostConfig); $containerCreatePostBody->setHostConfig($hostConfig);
} }
//handle withPrivilegedMode
if($this->isPrivileged) {
$hostConfig = new HostConfig();
$hostConfig->setPrivileged($this->isPrivileged);
}
$containerCreatePostBody->setImage($this->image); $containerCreatePostBody->setImage($this->image);
$containerCreatePostBody->setCmd($this->command); $containerCreatePostBody->setCmd($this->command);
$envs = []; $envs = [];
@@ -267,96 +239,13 @@ class GenericContainer
$this->dockerClient->containerStart($this->id); $this->dockerClient->containerStart($this->id);
if(!isset($this->wait)) { if(!isset($this->waitStrategy)) {
$this->withWait(new WaitForContainerRunning()); $this->withWait(new WaitForContainer());
} }
$this->wait->wait($this->id); $startedContainer = new StartedGenericContainer($this->id);
$this->waitStrategy->wait($startedContainer);
return $this; return $startedContainer;
}
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<string> $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<string, int>}
*/
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,
];
} }
} }
+6 -33
View File
@@ -4,39 +4,12 @@ declare(strict_types=1);
namespace Testcontainers\Container; namespace Testcontainers\Container;
use Testcontainers\Wait\WaitForLog; /**
* Left for namespace backward compatibility
class MariaDBContainer extends GenericContainer * @deprecated Use \Testcontainers\Modules\MariaDBContainer instead.
{ * TODO: Remove in next major release.
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 class MariaDBContainer extends \Testcontainers\Modules\MariaDBContainer
{ {
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;
}
} }
+6 -33
View File
@@ -4,39 +4,12 @@ declare(strict_types=1);
namespace Testcontainers\Container; namespace Testcontainers\Container;
use Testcontainers\Wait\WaitForLog; /**
* Left for namespace backward compatibility
class MySQLContainer extends GenericContainer * @deprecated Use \Testcontainers\Modules\MySQLContainer instead.
{ * TODO: Remove in next major release.
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 class MySQLContainer extends \Testcontainers\Modules\MySQLContainer
{ {
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;
}
} }
+6 -39
View File
@@ -4,45 +4,12 @@ declare(strict_types=1);
namespace Testcontainers\Container; namespace Testcontainers\Container;
use Testcontainers\Wait\WaitForHttp; /**
use Testcontainers\Wait\WaitForLog; * Left for namespace backward compatibility
* @deprecated Use \Testcontainers\Modules\OpenSearchContainer instead.
class OpenSearchContainer extends GenericContainer * 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();
}
} }
+6 -41
View File
@@ -4,47 +4,12 @@ declare(strict_types=1);
namespace Testcontainers\Container; namespace Testcontainers\Container;
use Testcontainers\Wait\WaitForExec; /**
* Left for namespace backward compatibility
class PostgresContainer extends GenericContainer * @deprecated Use \Testcontainers\Modules\PostgresContainer instead.
{ * TODO: Remove in next major release.
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 class PostgresContainer extends \Testcontainers\Modules\PostgresContainer
{ {
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;
}
} }
+7 -18
View File
@@ -4,23 +4,12 @@ declare(strict_types=1);
namespace Testcontainers\Container; namespace Testcontainers\Container;
use Testcontainers\Wait\WaitForLog; /**
* Left for namespace backward compatibility
class RedisContainer extends GenericContainer * @deprecated Use \Testcontainers\Modules\RedisContainer instead.
{ * TODO: Remove in next major release.
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 class RedisContainer extends \Testcontainers\Modules\RedisContainer
{ {
return new self($version);
}
} }
+152
View File
@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Container;
use Docker\API\Client;
use Docker\API\Model\ContainersIdExecPostBody;
use Docker\API\Model\IdResponse;
use Docker\API\Runtime\Client\Client as DockerRuntimeClient;
use Docker\Docker;
use Testcontainers\ContainerClient\DockerContainerClient;
class StartedGenericContainer implements StartedTestContainer
{
protected Docker $dockerClient;
protected ?string $lastExecId = null;
public function __construct(protected readonly string $id)
{
$this->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<string> $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 '';
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Container;
use Docker\Docker;
interface StartedTestContainer
{
public function stop(): StoppedTestContainer;
public function restart(): self;
public function getClient(): Docker;
public function getHost(): string;
public function getFirstMappedPort(): int;
public function getMappedPort(int $port): int;
public function getName(): string;
public function getLabels(): array;
public function getId(): string;
public function getLastExecId(): string | null;
public function getNetworkNames(): array;
public function getNetworkId(string $networkName): string;
public function getIpAddress(string $networkName): string;
/**
* @param list<string> $command
*/
public function exec(array $command): string;
public function logs(): string;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Container;
class StoppedGenericContainer implements StoppedTestContainer
{
public function __construct(protected readonly string $id)
{
}
public function getId(): string
{
return $this->id;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Container;
interface StoppedTestContainer
{
public function getId(): string;
}
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Container;
use Testcontainers\Wait\WaitStrategy;
interface TestContainer
{
public function start(): StartedGenericContainer;
/**
* TODO: replace with array after deprecated implementation is removed
* @param array<string, string>|string $env
*/
public function withEnvironment(array | string $env, ?string $value): static;
/**
* @param array<string> $command
*/
public function withCommand(array $command): static;
public function withEntrypoint(string $entryPoint): static;
/** @param int|string|array<int|string> $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;
}
@@ -1,10 +1,10 @@
<?php <?php
namespace Testcontainers\ContainerRuntime; namespace Testcontainers\ContainerClient;
use Docker\Docker as DockerClient; use Docker\Docker as DockerClient;
class ContainerRuntimeClient class DockerContainerClient
{ {
/** /**
* @var DockerClient|null Singleton instance of DockerClient * @var DockerClient|null Singleton instance of DockerClient
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Modules;
use Testcontainers\Container\GenericContainer;
use Testcontainers\Wait\WaitForLog;
class MariaDBContainer extends GenericContainer
{
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;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Modules;
use Testcontainers\Container\GenericContainer;
use Testcontainers\Wait\WaitForLog;
class MySQLContainer extends GenericContainer
{
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;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Modules;
use Testcontainers\Container\GenericContainer;
use Testcontainers\Wait\WaitForLog;
class OpenSearchContainer extends GenericContainer
{
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();
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Modules;
use Testcontainers\Container\GenericContainer;
use Testcontainers\Wait\WaitForExec;
class PostgresContainer extends GenericContainer
{
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;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Modules;
use Testcontainers\Container\GenericContainer;
use Testcontainers\Wait\WaitForLog;
class RedisContainer extends GenericContainer
{
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);
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Utils\PortGenerator;
class FixedPortGenerator implements PortGenerator
{
protected int $portIndex = 0;
public function __construct(
/** @var int[] */
protected array $ports
) {
}
public function generatePort(): int
{
if (!isset($this->ports[$this->portIndex])) {
throw new \RuntimeException('No more ports available in the fixed list.');
}
return $this->ports[$this->portIndex++];
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Utils\PortGenerator;
interface PortGenerator
{
public function generatePort(): int;
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Utils\PortGenerator;
class RandomPortGenerator implements PortGenerator
{
public function generatePort(): int
{
return $this->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;
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Utils\PortGenerator;
class RandomUniquePortGenerator implements PortGenerator
{
/** @var int[] */
protected static array $assignedPorts = [];
public function __construct(protected PortGenerator $portGenerator = new RandomPortGenerator())
{
}
public function generatePort(): int
{
do {
$port = $this->portGenerator->generatePort();
} while (in_array($port, self::$assignedPorts));
self::$assignedPorts[] = $port;
return $port;
}
}
-20
View File
@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Wait;
use Docker\Docker;
use Testcontainers\ContainerRuntime\ContainerRuntimeClient;
abstract class BaseWait implements WaitInterface
{
protected Docker $dockerClient;
public function __construct(protected int $timeout = 10000, protected int $pollInterval = 500)
{
$this->dockerClient = ContainerRuntimeClient::getDockerClient();
}
abstract public function wait(string $id): void;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Wait;
use Testcontainers\Container\StartedTestContainer;
abstract class BaseWaitStrategy implements WaitStrategy
{
public function __construct(protected int $timeout = 10000, protected int $pollInterval = 500)
{
}
abstract public function wait(StartedTestContainer $container): void;
}
@@ -5,16 +5,18 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use Docker\API\Model\ContainersIdJsonGetResponse200; use Docker\API\Model\ContainersIdJsonGetResponse200;
use Testcontainers\Container\StartedTestContainer;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
/** /**
* Simply makes container inspect and checks if container is running. * Simply makes container inspect and checks if container is running.
* Uses $timout and $pollInterval in milliseconds to set the parameters for waiting. * Uses $timout and $pollInterval in milliseconds to set the parameters for waiting.
*/ */
class WaitForContainerRunning extends BaseWait class WaitForContainer extends BaseWaitStrategy
{ {
public function wait(string $id): void public function wait(StartedTestContainer $container): void
{ {
$id = $container->getId();
$startTime = microtime(true) * 1000; $startTime = microtime(true) * 1000;
while (true) { while (true) {
@@ -25,7 +27,7 @@ class WaitForContainerRunning extends BaseWait
} }
/** @var ContainersIdJsonGetResponse200 | null $containerInspect */ /** @var ContainersIdJsonGetResponse200 | null $containerInspect */
$containerInspect = $this->dockerClient->containerInspect($id); $containerInspect = $container->getClient()->containerInspect($id);
$containerStatus = $containerInspect?->getState()?->getStatus(); $containerStatus = $containerInspect?->getState()?->getStatus();
if ($containerStatus === 'running') { if ($containerStatus === 'running') {
+9 -17
View File
@@ -5,14 +5,15 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use Closure; use Closure;
use Docker\API\Client;
use Docker\API\Model\ContainersIdExecPostBody; use Docker\API\Model\ContainersIdExecPostBody;
use Docker\API\Model\ExecIdJsonGetResponse200;
use Testcontainers\Container\StartedTestContainer;
use Testcontainers\Exception\ContainerWaitingTimeoutException; use Testcontainers\Exception\ContainerWaitingTimeoutException;
/** /**
* Uses $timout and $pollInterval in milliseconds to set the parameters for waiting. * Uses $timout and $pollInterval in milliseconds to set the parameters for waiting.
*/ */
class WaitForExec extends BaseWait class WaitForExec extends BaseWaitStrategy
{ {
protected ContainersIdExecPostBody $execConfig; protected ContainersIdExecPostBody $execConfig;
@@ -28,32 +29,23 @@ class WaitForExec extends BaseWait
parent::__construct($timeout, $pollInterval); 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; $startTime = microtime(true) * 1000;
while (true) { while (true) {
$elapsedTime = (microtime(true) * 1000) - $startTime; $elapsedTime = (microtime(true) * 1000) - $startTime;
if ($elapsedTime > $this->timeout) { if ($elapsedTime > $this->timeout) {
throw new ContainerWaitingTimeoutException($id); throw new ContainerWaitingTimeoutException($container->getId());
} }
// Create and start the exec command $contents = $container->exec($this->command);
$exec = $this->dockerClient->containerExec($id, $this->execConfig);
$contents = $this->dockerClient
->execStart($exec->getId(), null, Client::FETCH_RESPONSE)
?->getBody()
->getContents() ?? '';
// Inspect the exec to check the exit code // Inspect the exec to check the exit code
$execInspect = $this->dockerClient->execInspect($exec->getId()); /** @var ExecIdJsonGetResponse200 | null $execInspect */
$exitCode = $execInspect->getExitCode(); $execInspect = $container->getClient()->execInspect($container->getLastExecId() ?? '');
$exitCode = $execInspect?->getExitCode();
// If a custom check function is provided, use it to validate the command output // If a custom check function is provided, use it to validate the command output
if ($this->checkFunction !== null) { if ($this->checkFunction !== null) {
+8 -14
View File
@@ -5,25 +5,18 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use Docker\Docker; use Docker\Docker;
use Docker\DockerClientFactory;
use Http\Client\Socket\Exception\TimeoutException; use Http\Client\Socket\Exception\TimeoutException;
use Testcontainers\ContainerRuntime\ContainerRuntimeClient; use Testcontainers\Container\StartedTestContainer;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
class WaitForHealthCheck implements WaitInterface class WaitForHealthCheck extends BaseWaitStrategy
{ {
protected Docker $dockerClient; public function __construct(protected int $timeout = 5000, protected int $pollInterval = 1000)
protected int $timeout;
protected int $pollInterval;
public function __construct(int $timeout = 5000, int $pollInterval = 1000)
{ {
$this->dockerClient = ContainerRuntimeClient::getDockerClient(); parent::__construct($timeout, $pollInterval);
$this->timeout = $timeout;
$this->pollInterval = $pollInterval;
} }
public function wait(string $id): void public function wait(StartedTestContainer $container): void
{ {
$startTime = microtime(true) * 1000; $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)); 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; //$containerStatus = $containerInspect?->getArrayCopy() ?? null;
var_dump($containerInspect->getBody()->getContents()); var_dump($containerInspect->getBody()->getContents());
$containerStatus=''; $containerStatus = '';
if ($containerStatus === 'healthy') { if ($containerStatus === 'healthy') {
return; return;
} }
+1 -1
View File
@@ -7,7 +7,7 @@ namespace Testcontainers\Wait;
use Docker\Docker; use Docker\Docker;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
class WaitForHttp implements WaitInterface class WaitForHttp implements WaitStrategy
{ {
public const METHOD_GET = 'GET'; public const METHOD_GET = 'GET';
public const METHOD_POST = 'POST'; public const METHOD_POST = 'POST';
+5 -10
View File
@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use Docker\API\Runtime\Client\Client; use Testcontainers\Container\StartedTestContainer;
use Testcontainers\Exception\ContainerWaitingTimeoutException; use Testcontainers\Exception\ContainerWaitingTimeoutException;
/** /**
* Uses $timout and $pollInterval in milliseconds to set the parameters for waiting. * Uses $timout and $pollInterval in milliseconds to set the parameters for waiting.
*/ */
class WaitForLog extends BaseWait class WaitForLog extends BaseWaitStrategy
{ {
public function __construct( public function __construct(
protected string $message, protected string $message,
@@ -21,7 +21,7 @@ class WaitForLog extends BaseWait
parent::__construct($timeout, $pollInterval); parent::__construct($timeout, $pollInterval);
} }
public function wait(string $id): void public function wait(StartedTestContainer $container): void
{ {
$startTime = microtime(true) * 1000; $startTime = microtime(true) * 1000;
@@ -29,15 +29,10 @@ class WaitForLog extends BaseWait
$elapsedTime = (microtime(true) * 1000) - $startTime; $elapsedTime = (microtime(true) * 1000) - $startTime;
if ($elapsedTime > $this->timeout) { if ($elapsedTime > $this->timeout) {
throw new ContainerWaitingTimeoutException($id); throw new ContainerWaitingTimeoutException($container->getId());
} }
$output = $this->dockerClient $output = $container->logs();
->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')) ?? '';
if ($this->enableRegex) { if ($this->enableRegex) {
if (preg_match($this->message, $output)) { if (preg_match($this->message, $output)) {
+1 -1
View File
@@ -9,7 +9,7 @@ use JsonException;
use RuntimeException; use RuntimeException;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
final class WaitForTcpPortOpen implements WaitInterface final class WaitForTcpPortOpen implements WaitStrategy
{ {
private Docker $dockerClient; private Docker $dockerClient;
-10
View File
@@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Wait;
interface WaitInterface
{
public function wait(string $id): void;
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Wait;
use Testcontainers\Container\StartedTestContainer;
interface WaitStrategy
{
public function wait(StartedTestContainer $container): void;
}
+3 -3
View File
@@ -5,14 +5,14 @@ declare(strict_types=1);
namespace Testcontainers\Tests\Integration; namespace Testcontainers\Tests\Integration;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Testcontainers\Container\GenericContainer; use Testcontainers\Container\StartedTestContainer;
abstract class ContainerTestCase extends TestCase abstract class ContainerTestCase extends TestCase
{ {
protected static GenericContainer $container; protected static StartedTestContainer $container;
protected function tearDown(): void protected function tearDown(): void
{ {
self::$container->remove(); self::$container->stop();
} }
} }
+6 -2
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Testcontainers\Tests\Integration; namespace Testcontainers\Tests\Integration;
use Testcontainers\Container\MariaDBContainer; use Testcontainers\Modules\MariaDBContainer;
class MariaDBContainerTest extends ContainerTestCase class MariaDBContainerTest extends ContainerTestCase
{ {
@@ -19,7 +19,11 @@ class MariaDBContainerTest extends ContainerTestCase
public function testMariaDBContainer(): void public function testMariaDBContainer(): void
{ {
$pdo = new \PDO( $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', 'bar',
'baz', 'baz',
); );
+6 -2
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Testcontainers\Tests\Integration; namespace Testcontainers\Tests\Integration;
use Testcontainers\Container\MySQLContainer; use Testcontainers\Modules\MySQLContainer;
class MySQLContainerTest extends ContainerTestCase class MySQLContainerTest extends ContainerTestCase
{ {
@@ -19,7 +19,11 @@ class MySQLContainerTest extends ContainerTestCase
public function testMySQLContainer(): void public function testMySQLContainer(): void
{ {
$pdo = new \PDO( $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', 'bar',
'baz', 'baz',
); );
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Tests\Integration\OldTests;
use PHPUnit\Framework\TestCase;
use Predis\Client;
use Testcontainers\Modules\MariaDBContainer;
use Testcontainers\Modules\MySQLContainer;
use Testcontainers\Modules\OpenSearchContainer;
use Testcontainers\Modules\PostgresContainer;
use Testcontainers\Modules\RedisContainer;
/**
* Old test classes kept to check backward compatibility
*/
class ContainerTest extends TestCase
{
public function testMySQL(): void
{
$container = MySQLContainer::make();
$container->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();
}
}
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Tests\Integration\OldTests;
use PHPUnit\Framework\TestCase;
use Predis\Client;
use Predis\Connection\ConnectionException;
use Testcontainers\Container\GenericContainer;
use Testcontainers\Exception\ContainerNotReadyException;
use Testcontainers\Wait\WaitForExec;
use Testcontainers\Wait\WaitForHealthCheck;
use Testcontainers\Wait\WaitForHttp;
use Testcontainers\Wait\WaitForLog;
use Testcontainers\Wait\WaitForTcpPortOpen;
/**
* Old test classes kept to check backward compatibility
*/
class WaitStrategyTest extends TestCase
{
public function testWaitForExec(): void
{
$container = GenericContainer::make('mysql')
->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<string, array<bool>>
// */
// 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);
// }
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Testcontainers\Tests\Integration; namespace Testcontainers\Tests\Integration;
use Testcontainers\Container\OpenSearchContainer; use Testcontainers\Modules\OpenSearchContainer;
class OpenSearchContainerTest extends ContainerTestCase class OpenSearchContainerTest extends ContainerTestCase
{ {
@@ -21,7 +21,11 @@ class OpenSearchContainerTest extends ContainerTestCase
public function testOpenSearch(): void public function testOpenSearch(): void
{ {
$ch = curl_init(); $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); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = (string) curl_exec($ch); $response = (string) curl_exec($ch);
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Testcontainers\Tests\Integration; namespace Testcontainers\Tests\Integration;
use Testcontainers\Container\PostgresContainer; use Testcontainers\Modules\PostgresContainer;
class PostgreSQLContainerTest extends ContainerTestCase class PostgreSQLContainerTest extends ContainerTestCase
{ {
@@ -19,7 +19,11 @@ class PostgreSQLContainerTest extends ContainerTestCase
public function testPostgreSQLContainer(): void public function testPostgreSQLContainer(): void
{ {
$pdo = new \PDO( $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', 'bar',
'test', 'test',
); );
+3 -3
View File
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Testcontainers\Tests\Integration; namespace Testcontainers\Tests\Integration;
use Predis\Client; use Predis\Client;
use Testcontainers\Container\RedisContainer; use Testcontainers\Modules\RedisContainer;
class RedisContainerTest extends ContainerTestCase class RedisContainerTest extends ContainerTestCase
{ {
@@ -18,8 +18,8 @@ class RedisContainerTest extends ContainerTestCase
public function testRedisContainer(): void public function testRedisContainer(): void
{ {
$redisClient = new Client([ $redisClient = new Client([
'host' => 'localhost', 'host' => self::$container->getHost(),
'port' => 6379, 'port' => self::$container->getFirstMappedPort(),
]); ]);
$redisClient->ping(); $redisClient->ping();
-157
View File
@@ -1,157 +0,0 @@
<?php
declare(strict_types=1);
namespace Testcontainers\Tests\Integration;
use PHPUnit\Framework\TestCase;
use Predis\Client;
use Predis\Connection\ConnectionException;
use Symfony\Component\Process\Process;
use Testcontainers\Container\GenericContainer;
use Testcontainers\Exception\ContainerNotReadyException;
use Testcontainers\Wait\WaitForExec;
use Testcontainers\Wait\WaitForHealthCheck;
use Testcontainers\Wait\WaitForHttp;
use Testcontainers\Wait\WaitForLog;
use Testcontainers\Wait\WaitForTcpPortOpen;
class WaitStrategyTest extends TestCase
{
public static function tearDownAfterClass(): void
{
parent::tearDownAfterClass();
}
public function testWaitForExec(): void
{
$called = false;
$container = GenericContainer::make('mysql')
->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<string, array<bool>>
*/
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);
}
}