diff --git a/README.md b/README.md index c5fad87..788453f 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ use Testcontainers\Wait\WaitForExec; use Testcontainers\Wait\WaitForLog; use Testcontainers\Wait\WaitForHttp; use Testcontainers\Wait\WaitForHealthCheck; +use Testcontainers\Wait\WaitForHostPort; $container = new GenericContainer('nginx:alpine'); @@ -58,7 +59,10 @@ $container->withWait(new WaitForLog('Ready to accept connections')); // Wait for an http request to succeed -$container->withWait(WaitForHttp::make($port, $method = 'GET', $path = '/')); +$container->withWait(new WaitForHttp($port, $method = 'GET', $path = '/')); + +// Wait for all bound ports to be open +$container->withWait(new WaitForHostPort()); // Wait until the docker heartcheck is green $container->withWait(new WaitForHealthCheck()); diff --git a/src/Container/StartedGenericContainer.php b/src/Container/StartedGenericContainer.php index 259abd6..d3a8194 100644 --- a/src/Container/StartedGenericContainer.php +++ b/src/Container/StartedGenericContainer.php @@ -108,7 +108,7 @@ class StartedGenericContainer implements StartedTestContainer public function getMappedPort(int $port): int { - $ports = (array) $this->ports(); + $ports = (array) $this->getBoundPorts(); /** @var PortBinding | null $portBinding */ $portBinding = $ports["{$port}/tcp"][0] ?? null; $mappedPort = $portBinding?->getHostPort(); @@ -121,7 +121,7 @@ class StartedGenericContainer implements StartedTestContainer public function getFirstMappedPort(): int { - $ports = (array) $this->ports(); + $ports = (array) $this->getBoundPorts(); $port = array_key_first($ports); /** @var PortBinding | null $firstPortBinding */ $firstPortBinding = $ports[$port][0] ?? null; @@ -193,10 +193,10 @@ class StartedGenericContainer implements StartedTestContainer } /** - * @return array> + * @return iterable> * @throws RuntimeException */ - protected function ports(): iterable + public function getBoundPorts(): iterable { $ports = $this->inspect()?->getNetworkSettings()?->getPorts(); diff --git a/src/Container/StartedTestContainer.php b/src/Container/StartedTestContainer.php index 8d1acd3..551d9be 100644 --- a/src/Container/StartedTestContainer.php +++ b/src/Container/StartedTestContainer.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Testcontainers\Container; +use Docker\API\Model\PortBinding; use Docker\Docker; interface StartedTestContainer @@ -13,6 +14,11 @@ interface StartedTestContainer */ public function exec(array $command): string; + /** + * @return iterable> + */ + public function getBoundPorts(): iterable; + public function getClient(): Docker; public function getFirstMappedPort(): int; diff --git a/src/Modules/MariaDBContainer.php b/src/Modules/MariaDBContainer.php index c6603d4..b8c868e 100644 --- a/src/Modules/MariaDBContainer.php +++ b/src/Modules/MariaDBContainer.php @@ -18,7 +18,7 @@ class MariaDBContainer extends GenericContainer "mariadb-admin", "ping", "-h", "127.0.0.1", - ])); + ], null, 15000)); } public function withMariaDBUser(string $username, string $password): self diff --git a/src/Modules/MySQLContainer.php b/src/Modules/MySQLContainer.php index 283c999..580dfd8 100644 --- a/src/Modules/MySQLContainer.php +++ b/src/Modules/MySQLContainer.php @@ -18,7 +18,7 @@ class MySQLContainer extends GenericContainer "mysqladmin", "ping", "-h", "127.0.0.1", - ])); + ], null, 15000)); } public function withMySQLUser(string $username, string $password): self diff --git a/src/Wait/WaitForHostPort.php b/src/Wait/WaitForHostPort.php index d9f7f86..9a97388 100644 --- a/src/Wait/WaitForHostPort.php +++ b/src/Wait/WaitForHostPort.php @@ -9,18 +9,9 @@ use Testcontainers\Exception\ContainerWaitingTimeoutException; class WaitForHostPort extends BaseWaitStrategy { - public function __construct( - protected int $port, - int $timeout = 10000, - int $pollInterval = 500 - ) { - parent::__construct($timeout, $pollInterval); - } - public function wait(StartedTestContainer $container): void { $startTime = microtime(true) * 1000; - $containerAddress = $container->getHost(); while (true) { $elapsedTime = (microtime(true) * 1000) - $startTime; @@ -29,7 +20,7 @@ class WaitForHostPort extends BaseWaitStrategy throw new ContainerWaitingTimeoutException($container->getId()); } - if ($this->isPortOpen($containerAddress, $this->port)) { + if ($this->boundPortsOpened($container)) { return; // Port is open, container is ready } @@ -37,6 +28,28 @@ class WaitForHostPort extends BaseWaitStrategy } } + /** + * @param StartedTestContainer $container + * @return bool + */ + private function boundPortsOpened(StartedTestContainer $container): bool + { + $boundPorts = $container->getBoundPorts(); + foreach ($boundPorts as $bindings) { + foreach ($bindings as $binding) { + $hostIp = trim($binding->getHostIp() ?? ''); + if ($hostIp === '' || $hostIp === '0.0.0.0') { + $hostIp = $container->getHost(); + } + $hostPort = (int)$binding->getHostPort(); + if (!$this->isPortOpen($hostIp, $hostPort)) { + return false; + } + } + } + return true; + } + private function isPortOpen(string $ipAddress, int $port): bool { $connection = @fsockopen($ipAddress, $port, $errno, $errstr, 2); diff --git a/src/Wait/WaitForHttp.php b/src/Wait/WaitForHttp.php index 3ae06c2..b2dceeb 100644 --- a/src/Wait/WaitForHttp.php +++ b/src/Wait/WaitForHttp.php @@ -38,16 +38,6 @@ class WaitForHttp extends BaseWaitStrategy parent::__construct($timeout, $pollInterval); } - /** - * @deprecated Use constructor instead - * Kept for backward compatibility - * Should be removed in next major version - */ - public static function make(int $port): self - { - return new self($port); - } - /** * @param HttpMethod|value-of $method */ diff --git a/tests/Integration/GenericContainerTest.php b/tests/Integration/GenericContainerTest.php index 7e00fc8..7ed65bb 100644 --- a/tests/Integration/GenericContainerTest.php +++ b/tests/Integration/GenericContainerTest.php @@ -7,7 +7,6 @@ namespace Testcontainers\Tests\Integration; use Docker\API\Model\ContainersIdJsonGetResponse200; use PHPUnit\Framework\TestCase; use Testcontainers\Container\GenericContainer; -use Testcontainers\Utils\PortGenerator\FixedPortGenerator; use Testcontainers\Wait\WaitForHostPort; class GenericContainerTest extends TestCase @@ -115,13 +114,12 @@ class GenericContainerTest extends TestCase public function testShouldReturnFirstMappedPort(): void { $container = (new GenericContainer('nginx')) - ->withPortGenerator(new FixedPortGenerator([9950])) ->withExposedPorts(80) - ->withWait(new WaitForHostPort(9950)) + ->withWait(new WaitForHostPort()) ->start(); $firstMappedPort = $container->getFirstMappedPort(); - self::assertSame($firstMappedPort, 9950, 'First mapped port does not match 9950'); + self::assertSame($firstMappedPort, $container->getMappedPort(80)); $container->stop(); }