mirror of
https://github.com/stan220/testcontainers-php.git
synced 2026-09-08 16:29:29 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
/vendor/
|
||||||
|
.php-cs-fixer.cache
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This document has been generated with
|
||||||
|
* https://mlocati.github.io/php-cs-fixer-configurator/#version:3.12.0|configurator
|
||||||
|
* you can change this configuration by importing this file.
|
||||||
|
*/
|
||||||
|
$config = new \PhpCsFixer\Config();
|
||||||
|
return $config
|
||||||
|
->setRules([
|
||||||
|
'@PSR12' => true,
|
||||||
|
'@PHP81Migration' => true,
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
])
|
||||||
|
->setFinder(PhpCsFixer\Finder::create()
|
||||||
|
->exclude('vendor')
|
||||||
|
->in(__DIR__)
|
||||||
|
)
|
||||||
|
;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Testcontainers for PHP
|
||||||
|
|
||||||
|
Testcontainers is a PHP package that makes it simple to create and clean up container-based dependencies for automated integration/smoke tests. The package is inspired by the [Testcontainers](https://www.testcontainers.org/) project for Java.
|
||||||
|
|
||||||
|
[@sironheart](https://github.com/sironheart) has annoyed me to test testcontainers, but it didn't existed in PHP yet.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add this to your project with composer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer req --dev shyim/testcontainer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage/Examples
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Testcontainer\Container\MySQLContainer;
|
||||||
|
|
||||||
|
$container = new MySQLContainer('8.0');
|
||||||
|
$container->withMySQLDatabase('foo');
|
||||||
|
$container->withMySQLUser('bar', 'baz');
|
||||||
|
|
||||||
|
$container->run();
|
||||||
|
|
||||||
|
$pdo = new \PDO(
|
||||||
|
sprintf('mysql:host=%s;port=3306', $container->getAddress()),
|
||||||
|
'bar',
|
||||||
|
'baz',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do something with pdo
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://choosealicense.com/licenses/mit/)
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "shyim/testcontainer",
|
||||||
|
"description": "Testcontainer implementation in PHP",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "library",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Soner Sayakci",
|
||||||
|
"email": "github@shyim.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": ">= 8.1",
|
||||||
|
"symfony/process": "^5.3|^6.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.5",
|
||||||
|
"brianium/paratest": "^6.6",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.12",
|
||||||
|
"phpstan/phpstan": "^1.8",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.1",
|
||||||
|
"phpstan/extension-installer": "^1.2"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Testcontainer\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Testcontainer\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"integration": "paratest tests/ --bootstrap vendor/autoload.php -f",
|
||||||
|
"cs": "php-cs-fixer fix --dry-run",
|
||||||
|
"cs:fix": "php-cs-fixer fix",
|
||||||
|
"phpstan": "phpstan analyse"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"phpstan/extension-installer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+4020
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
parameters:
|
||||||
|
level: 9
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Container;
|
||||||
|
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Testcontainer\Exception\ContainerNotReadyException;
|
||||||
|
use Testcontainer\Registry;
|
||||||
|
use Testcontainer\Wait\WaitForNothing;
|
||||||
|
use Testcontainer\Wait\WaitInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-type ContainerInspect array{0: array{NetworkSettings: array{IPAddress: string}}}
|
||||||
|
*/
|
||||||
|
class Container
|
||||||
|
{
|
||||||
|
private string $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private array $env = [];
|
||||||
|
|
||||||
|
private Process $process;
|
||||||
|
private WaitInterface $wait;
|
||||||
|
|
||||||
|
private ?string $healthCheckCommand = null;
|
||||||
|
private int $healthCheckIntervalInMS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ContainerInspect
|
||||||
|
*/
|
||||||
|
private array $inspectedData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string>
|
||||||
|
*/
|
||||||
|
private array $mounts = [];
|
||||||
|
|
||||||
|
public function __construct(private string $image)
|
||||||
|
{
|
||||||
|
$this->wait = new WaitForNothing();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function make(string $image): self
|
||||||
|
{
|
||||||
|
return new Container($image);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withEnvironment(string $name, string $value): self
|
||||||
|
{
|
||||||
|
$this->env[$name] = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withImage(string $image): self
|
||||||
|
{
|
||||||
|
$this->image = $image;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withWait(WaitInterface $wait): self
|
||||||
|
{
|
||||||
|
$this->wait = $wait;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withHealthCheckCommand(string $command, int $healthCheckIntervalInMS = 1000): self
|
||||||
|
{
|
||||||
|
$this->healthCheckCommand = $command;
|
||||||
|
$this->healthCheckIntervalInMS = $healthCheckIntervalInMS;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withMount(string $localPath, string $containerPath): self
|
||||||
|
{
|
||||||
|
$this->mounts[] = '-v';
|
||||||
|
$this->mounts[] = sprintf('%s:%s', $localPath, $containerPath);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(bool $wait = true): self
|
||||||
|
{
|
||||||
|
$this->id = uniqid('testcontainer', true);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'docker',
|
||||||
|
'run',
|
||||||
|
'--rm',
|
||||||
|
'--detach',
|
||||||
|
'--name',
|
||||||
|
$this->id,
|
||||||
|
...$this->mounts,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->env as $name => $value) {
|
||||||
|
$params[] = '--env';
|
||||||
|
$params[] = $name . '=' . $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->healthCheckCommand !== null) {
|
||||||
|
$params[] = '--health-cmd';
|
||||||
|
$params[] = $this->healthCheckCommand;
|
||||||
|
$params[] = '--health-interval';
|
||||||
|
$params[] = $this->healthCheckIntervalInMS . 'ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[] = $this->image;
|
||||||
|
|
||||||
|
$this->process = new Process($params);
|
||||||
|
$this->process->mustRun();
|
||||||
|
|
||||||
|
$inspect = new Process(['docker', 'inspect', $this->id]);
|
||||||
|
$inspect->mustRun();
|
||||||
|
|
||||||
|
/** @var ContainerInspect $inspectedData */
|
||||||
|
$inspectedData = json_decode($inspect->getOutput(), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
$this->inspectedData = $inspectedData;
|
||||||
|
|
||||||
|
Registry::add($this);
|
||||||
|
|
||||||
|
if ($wait) {
|
||||||
|
$this->wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wait(int $wait = 100): self
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < $wait; $i++) {
|
||||||
|
try {
|
||||||
|
$this->wait->wait($this->id);
|
||||||
|
return $this;
|
||||||
|
} catch (ContainerNotReadyException $e) {
|
||||||
|
usleep(500000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ContainerNotReadyException($this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stop(): self
|
||||||
|
{
|
||||||
|
$stop = new Process(['docker', 'stop', $this->id]);
|
||||||
|
$stop->mustRun();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function start(): self
|
||||||
|
{
|
||||||
|
$start = new Process(['docker', 'start', $this->id]);
|
||||||
|
$start->mustRun();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restart(): self
|
||||||
|
{
|
||||||
|
$restart = new Process(['docker', 'restart', $this->id]);
|
||||||
|
$restart->mustRun();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(): self
|
||||||
|
{
|
||||||
|
$remove = new Process(['docker', 'rm', '-f', $this->id]);
|
||||||
|
$remove->mustRun();
|
||||||
|
|
||||||
|
Registry::remove($this);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kill(): self
|
||||||
|
{
|
||||||
|
$kill = new Process(['docker', 'kill', $this->id]);
|
||||||
|
$kill->mustRun();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string> $command
|
||||||
|
*/
|
||||||
|
public function execute(array $command): Process
|
||||||
|
{
|
||||||
|
$process = new Process(['docker', 'exec', $this->id, ...$command]);
|
||||||
|
$process->mustRun();
|
||||||
|
|
||||||
|
return $process;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logs(): string
|
||||||
|
{
|
||||||
|
$logs = new Process(['docker', 'logs', $this->id]);
|
||||||
|
$logs->mustRun();
|
||||||
|
|
||||||
|
return $logs->getOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAddress(): string
|
||||||
|
{
|
||||||
|
return $this->inspectedData[0]['NetworkSettings']['IPAddress'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Container;
|
||||||
|
|
||||||
|
use Testcontainer\Wait\WaitForExec;
|
||||||
|
|
||||||
|
class MariaDBContainer extends Container
|
||||||
|
{
|
||||||
|
public function __construct(string $version = 'latest', string $mysqlRootPassword = 'root')
|
||||||
|
{
|
||||||
|
parent::__construct('mariadb:' . $version);
|
||||||
|
$this->withEnvironment('MARIADB_ROOT_PASSWORD', $mysqlRootPassword);
|
||||||
|
$this->withWait(new WaitForExec(['mysqladmin', 'ping', '-h', '127.0.0.1']));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Container;
|
||||||
|
|
||||||
|
use Testcontainer\Wait\WaitForExec;
|
||||||
|
|
||||||
|
class MySQLContainer extends Container
|
||||||
|
{
|
||||||
|
public function __construct(string $version = 'latest', string $mysqlRootPassword = 'root')
|
||||||
|
{
|
||||||
|
parent::__construct('mysql:' . $version);
|
||||||
|
$this->withEnvironment('MYSQL_ROOT_PASSWORD', $mysqlRootPassword);
|
||||||
|
$this->withWait(new WaitForExec(['mysqladmin', 'ping', '-h', '127.0.0.1']));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Container;
|
||||||
|
|
||||||
|
use Testcontainer\Wait\WaitForHttp;
|
||||||
|
|
||||||
|
class OpenSearchContainer extends Container
|
||||||
|
{
|
||||||
|
public function __construct(string $version = 'latest')
|
||||||
|
{
|
||||||
|
parent::__construct('opensearchproject/opensearch:' . $version);
|
||||||
|
$this->withEnvironment('discovery.type', 'single-node');
|
||||||
|
$this->withWait(WaitForHttp::make(9200));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function make(string $version = 'latest'): self
|
||||||
|
{
|
||||||
|
return new self($version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disableSecurityPlugin(): self
|
||||||
|
{
|
||||||
|
$this->withEnvironment('plugins.security.disabled', 'true');
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Container;
|
||||||
|
|
||||||
|
use Testcontainer\Wait\WaitForLog;
|
||||||
|
|
||||||
|
class RedisContainer extends Container
|
||||||
|
{
|
||||||
|
public function __construct(string $version = 'latest')
|
||||||
|
{
|
||||||
|
parent::__construct('redis:' . $version);
|
||||||
|
$this->withWait(new WaitForLog('Ready to accept connections'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function make(string $version = 'latest'): self
|
||||||
|
{
|
||||||
|
return new self($version);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Exception;
|
||||||
|
|
||||||
|
class ContainerNotReadyException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(string $id, ?\Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('Container %s is not ready', $id), 0, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer;
|
||||||
|
|
||||||
|
use Testcontainer\Container\Container;
|
||||||
|
|
||||||
|
class Registry
|
||||||
|
{
|
||||||
|
private static bool $registeredCleanup = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int|string, Container>
|
||||||
|
*/
|
||||||
|
private static array $registry = [];
|
||||||
|
|
||||||
|
public static function add(Container $container): void
|
||||||
|
{
|
||||||
|
self::$registry[spl_object_id($container)] = $container;
|
||||||
|
|
||||||
|
if (!self::$registeredCleanup) {
|
||||||
|
register_shutdown_function([self::class, 'cleanup']);
|
||||||
|
self::$registeredCleanup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function remove(Container $container): void
|
||||||
|
{
|
||||||
|
unset(self::$registry[spl_object_id($container)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function cleanup(): void
|
||||||
|
{
|
||||||
|
foreach (self::$registry as $container) {
|
||||||
|
$container->remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Wait;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Testcontainer\Exception\ContainerNotReadyException;
|
||||||
|
|
||||||
|
class WaitForExec implements WaitInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string> $command
|
||||||
|
*/
|
||||||
|
public function __construct(private array $command, private ?Closure $checkFunction = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wait(string $id): void
|
||||||
|
{
|
||||||
|
$process = new Process(['docker', 'exec', $id, ...$this->command]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$process->mustRun();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new ContainerNotReadyException($id, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->checkFunction !== null) {
|
||||||
|
$func = $this->checkFunction;
|
||||||
|
$func($process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Wait;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Testcontainer\Exception\ContainerNotReadyException;
|
||||||
|
|
||||||
|
class WaitForHealthCheck implements WaitInterface
|
||||||
|
{
|
||||||
|
public function wait(string $id): void
|
||||||
|
{
|
||||||
|
$process = new Process(['docker', 'inspect', '--format', '{{json .State.Health.Status}}', $id]);
|
||||||
|
$process->mustRun();
|
||||||
|
|
||||||
|
$status = json_decode($process->getOutput(), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
if (!is_string($status)) {
|
||||||
|
throw new ContainerNotReadyException($id, new RuntimeException('Invalid json output'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = trim($status, '"');
|
||||||
|
|
||||||
|
if ($status !== 'healthy') {
|
||||||
|
throw new ContainerNotReadyException($id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Wait;
|
||||||
|
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Testcontainer\Exception\ContainerNotReadyException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-import-type ContainerInspect from \Testcontainer\Container\Container
|
||||||
|
*/
|
||||||
|
class WaitForHttp implements WaitInterface
|
||||||
|
{
|
||||||
|
public const METHOD_GET = 'GET';
|
||||||
|
public const METHOD_POST = 'POST';
|
||||||
|
public const METHOD_PUT = 'PUT';
|
||||||
|
public const METHOD_DELETE = 'DELETE';
|
||||||
|
public const METHOD_HEAD = 'HEAD';
|
||||||
|
public const METHOD_OPTIONS = 'OPTIONS';
|
||||||
|
|
||||||
|
|
||||||
|
private string $method = 'GET';
|
||||||
|
private string $path = '/';
|
||||||
|
private int $statusCode = 200;
|
||||||
|
|
||||||
|
public function __construct(private int $port)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function make(int $port): self
|
||||||
|
{
|
||||||
|
return new WaitForHttp($port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param WaitForHttp::METHOD_* $method
|
||||||
|
*/
|
||||||
|
public function withMethod(string $method): self
|
||||||
|
{
|
||||||
|
$this->method = $method;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPath(string $path): self
|
||||||
|
{
|
||||||
|
$this->path = $path;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withStatusCode(int $statusCode): self
|
||||||
|
{
|
||||||
|
$this->statusCode = $statusCode;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wait(string $id): void
|
||||||
|
{
|
||||||
|
$process = new Process(['docker', 'inspect', $id]);
|
||||||
|
$process->mustRun();
|
||||||
|
|
||||||
|
/** @var ContainerInspect $data */
|
||||||
|
$data = json_decode($process->getOutput(), true);
|
||||||
|
|
||||||
|
$ip = $data[0]['NetworkSettings']['IPAddress'];
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d%s', $ip, $this->port, $this->path));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
|
||||||
|
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_NOBODY, true);
|
||||||
|
|
||||||
|
curl_exec($ch);
|
||||||
|
|
||||||
|
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== $this->statusCode) {
|
||||||
|
throw new ContainerNotReadyException($id, new \RuntimeException('HTTP status code does not match'));
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_close($ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Wait;
|
||||||
|
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Testcontainer\Exception\ContainerNotReadyException;
|
||||||
|
|
||||||
|
class WaitForLog implements WaitInterface
|
||||||
|
{
|
||||||
|
public function __construct(private string $message, private bool $enableRegex = false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wait(string $id): void
|
||||||
|
{
|
||||||
|
$process = new Process(['docker', 'logs', $id]);
|
||||||
|
$process->mustRun();
|
||||||
|
|
||||||
|
$output = $process->getOutput() . PHP_EOL . $process->getErrorOutput();
|
||||||
|
|
||||||
|
if ($this->enableRegex) {
|
||||||
|
if (!preg_match($this->message, $output)) {
|
||||||
|
throw new ContainerNotReadyException($id, new \RuntimeException('Message not found in logs'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!str_contains($output, $this->message)) {
|
||||||
|
throw new ContainerNotReadyException($id, new \RuntimeException('Message not found in logs'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Wait;
|
||||||
|
|
||||||
|
class WaitForNothing implements WaitInterface
|
||||||
|
{
|
||||||
|
public function wait(string $id): void
|
||||||
|
{
|
||||||
|
// does nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Wait;
|
||||||
|
|
||||||
|
interface WaitInterface
|
||||||
|
{
|
||||||
|
public function wait(string $id): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Tests\Integration;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Redis;
|
||||||
|
use Testcontainer\Container\MariaDBContainer;
|
||||||
|
use Testcontainer\Container\MySQLContainer;
|
||||||
|
use Testcontainer\Container\OpenSearchContainer;
|
||||||
|
use Testcontainer\Container\RedisContainer;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRedis(): void
|
||||||
|
{
|
||||||
|
$container = RedisContainer::make();
|
||||||
|
|
||||||
|
$container->run();
|
||||||
|
|
||||||
|
$redis = new Redis();
|
||||||
|
$redis->connect($container->getAddress(), 6379, 0.001);
|
||||||
|
|
||||||
|
$redis->ping();
|
||||||
|
|
||||||
|
$this->assertTrue($redis->isConnected());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('cluster_name', $data);
|
||||||
|
|
||||||
|
$this->assertEquals('docker-cluster', $data['cluster_name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainer\Tests\Integration;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Testcontainer\Container\Container;
|
||||||
|
use Testcontainer\Wait\WaitForExec;
|
||||||
|
use Testcontainer\Wait\WaitForHealthCheck;
|
||||||
|
use Testcontainer\Wait\WaitForHttp;
|
||||||
|
use Testcontainer\Wait\WaitForLog;
|
||||||
|
|
||||||
|
class WaitStrategyTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testWaitForExec(): void
|
||||||
|
{
|
||||||
|
$called = false;
|
||||||
|
|
||||||
|
$container = Container::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 = Container::make('redis:6.2.5')
|
||||||
|
->withWait(new WaitForLog('Ready to accept connections'));
|
||||||
|
|
||||||
|
$container->run();
|
||||||
|
|
||||||
|
$redis = new \Redis();
|
||||||
|
$redis->connect($container->getAddress(), 6379, 0.001);
|
||||||
|
|
||||||
|
$redis->set('foo', 'bar');
|
||||||
|
|
||||||
|
$this->assertEquals('bar', $redis->get('foo'));
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
|
||||||
|
$this->expectException(\RedisException::class);
|
||||||
|
|
||||||
|
$redis->get('foo');
|
||||||
|
|
||||||
|
$container->remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWaitForHTTP(): void
|
||||||
|
{
|
||||||
|
$container = Container::make('opensearchproject/opensearch')
|
||||||
|
->withEnvironment('discovery.type', 'single-node')
|
||||||
|
->withEnvironment('plugins.security.disabled', 'true')
|
||||||
|
->withWait(WaitForHttp::make(9200));
|
||||||
|
|
||||||
|
$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);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('cluster_name', $data);
|
||||||
|
|
||||||
|
$this->assertEquals('docker-cluster', $data['cluster_name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWaitForHealthCheck(): void
|
||||||
|
{
|
||||||
|
$container = Container::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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user