diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 23c29bf..4273c5a 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -5,8 +5,6 @@ on: branches: - main pull_request: - branches: - - main permissions: contents: read 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/Utils/TarBuilder.php b/src/Utils/TarBuilder.php index ba615e7..40c5e37 100644 --- a/src/Utils/TarBuilder.php +++ b/src/Utils/TarBuilder.php @@ -136,9 +136,16 @@ class TarBuilder private function runTarCommand(string $tarFilePath, string $sourceDir): void { + if (PHP_OS_FAMILY === 'Darwin') { + $additionalFlags = ' --disable-copyfile --no-xattrs'; + } else { + $additionalFlags = ''; + } + // without --disable-copyfile and --no-xattrs combination, tar will fail on macOS $cmd = sprintf( - 'tar --no-xattrs --disable-copyfile -cf %s -C %s . 2>&1', + 'tar %s -cf %s -C %s . 2>&1', + $additionalFlags, escapeshellarg($tarFilePath), escapeshellarg($sourceDir) ); 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 88977cf..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 @@ -114,14 +113,13 @@ class GenericContainerTest extends TestCase public function testShouldReturnFirstMappedPort(): void { - $container = (new GenericContainer('cristianrgreco/testcontainer:1.1.14')) - ->withPortGenerator(new FixedPortGenerator([8080])) - ->withExposedPorts(8080) - ->withWait(new WaitForHostPort(8080)) + $container = (new GenericContainer('nginx')) + ->withExposedPorts(80) + ->withWait(new WaitForHostPort()) ->start(); $firstMappedPort = $container->getFirstMappedPort(); - self::assertSame($firstMappedPort, 8080, 'First mapped port does not match 8080'); + self::assertSame($firstMappedPort, $container->getMappedPort(80)); $container->stop(); }