mirror of
https://github.com/stan220/testcontainers-php.git
synced 2026-09-08 15:29:31 +00:00
Merge pull request #27 from rw4lll/feat/started-generic-container
Feat/started generic container
This commit is contained in:
+1
-1
@@ -45,7 +45,7 @@
|
||||
"integration:old": "phpunit tests/Integration/OldTests --bootstrap vendor/autoload.php",
|
||||
"cs": "php-cs-fixer fix --dry-run",
|
||||
"cs:fix": "php-cs-fixer fix",
|
||||
"phpstan": "phpstan analyse"
|
||||
"phpstan": "phpstan analyse --memory-limit=256M"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
||||
@@ -6,21 +6,27 @@ namespace Testcontainers\Container;
|
||||
|
||||
use Docker\API\Client;
|
||||
use Docker\API\Model\ContainersIdExecPostBody;
|
||||
use Docker\API\Model\ContainersIdJsonGetResponse200;
|
||||
use Docker\API\Model\EndpointSettings;
|
||||
use Docker\API\Model\IdResponse;
|
||||
use Docker\API\Model\PortBinding;
|
||||
use Docker\API\Runtime\Client\Client as DockerRuntimeClient;
|
||||
use Docker\Docker;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use RuntimeException;
|
||||
use Testcontainers\ContainerClient\DockerContainerClient;
|
||||
use Testcontainers\Utils\HostResolver;
|
||||
|
||||
class StartedGenericContainer implements StartedTestContainer
|
||||
{
|
||||
protected Docker $dockerClient;
|
||||
|
||||
protected ?ContainersIdJsonGetResponse200 $inspectResponse = null;
|
||||
|
||||
protected ?string $lastExecId = null;
|
||||
|
||||
public function __construct(protected readonly string $id)
|
||||
public function __construct(protected readonly string $id, ?Docker $dockerClient = null)
|
||||
{
|
||||
$this->dockerClient = DockerContainerClient::getDockerClient();
|
||||
$this->dockerClient = $dockerClient ?? DockerContainerClient::getDockerClient();
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
@@ -53,7 +59,7 @@ class StartedGenericContainer implements StartedTestContainer
|
||||
$exec = $this->dockerClient->containerExec($this->id, $execConfig);
|
||||
|
||||
if ($exec === null || $exec->getId() === null) {
|
||||
throw new \RuntimeException('Failed to create exec command');
|
||||
throw new RuntimeException('Failed to create exec command');
|
||||
}
|
||||
|
||||
$this->lastExecId = $exec->getId();
|
||||
@@ -63,7 +69,7 @@ class StartedGenericContainer implements StartedTestContainer
|
||||
?->getBody()
|
||||
->getContents() ?? '';
|
||||
|
||||
return preg_replace('/[\x00-\x1F\x7F]/u', '', $contents) ?? '';
|
||||
return $this->sanitizeOutput($contents);
|
||||
}
|
||||
|
||||
public function stop(): StoppedTestContainer
|
||||
@@ -92,81 +98,117 @@ class StartedGenericContainer implements StartedTestContainer
|
||||
?->getBody()
|
||||
->getContents() ?? '';
|
||||
|
||||
return preg_replace('/[\x00-\x1F\x7F]/u', '', mb_convert_encoding($output, 'UTF-8', 'UTF-8')) ?? '';
|
||||
return $this->sanitizeOutput(mb_convert_encoding($output, 'UTF-8', 'UTF-8'));
|
||||
}
|
||||
|
||||
//TODO: replace with the proper implementation
|
||||
public function getHost(): string
|
||||
{
|
||||
return '127.0.0.1';
|
||||
return (new HostResolver($this->dockerClient))->resolveHost();
|
||||
}
|
||||
|
||||
//TODO: not ready yet
|
||||
public function getMappedPort(int $port): int
|
||||
{
|
||||
return $this->inspect()->ports[$port];
|
||||
$ports = (array) $this->ports();
|
||||
/** @var PortBinding | null $portBinding */
|
||||
$portBinding = $ports["{$port}/tcp"][0] ?? null;
|
||||
$mappedPort = $portBinding?->getHostPort();
|
||||
if ($mappedPort !== null) {
|
||||
return (int) $mappedPort;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Failed to get mapped port ‘{$mappedPort}’ for container");
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function getFirstMappedPort(): int
|
||||
{
|
||||
//For some reason, containerInspect can crash when using FETCH_OBJECT option (e.g. with OpenSearch)
|
||||
//should be checked within beluga-php/docker-php client library
|
||||
/** @var ResponseInterface | null $containerInspectResponse */
|
||||
$containerInspectResponse = $this->dockerClient->containerInspect($this->id, [], Docker::FETCH_RESPONSE);
|
||||
if ($containerInspectResponse === null) {
|
||||
throw new \RuntimeException('Failed to inspect container');
|
||||
}
|
||||
|
||||
$containerInspectResponseAsArray = json_decode(
|
||||
$containerInspectResponse->getBody()->getContents(),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
|
||||
/** @var array<string, array<array<string, string>>> $ports */
|
||||
$ports = $containerInspectResponseAsArray['NetworkSettings']['Ports'] ?? [];
|
||||
|
||||
if ($ports === []) {
|
||||
throw new \RuntimeException('Failed to get ports from container');
|
||||
}
|
||||
|
||||
$ports = (array) $this->ports();
|
||||
$port = array_key_first($ports);
|
||||
/** @var PortBinding | null $firstPortBinding */
|
||||
$firstPortBinding = $ports[$port][0] ?? null;
|
||||
$firstMappedPort = $firstPortBinding?->getHostPort();
|
||||
if ($firstMappedPort !== null) {
|
||||
return (int) $firstMappedPort;
|
||||
}
|
||||
|
||||
return (int) $ports[$port][0]['HostPort'];
|
||||
throw new RuntimeException('Failed to get first mapped port for container');
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
// TODO: Implement getName() method.
|
||||
return '';
|
||||
return trim($this->inspect()?->getName() ?? '', '/ ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getLabels(): array
|
||||
{
|
||||
// TODO: Implement getLabels() method.
|
||||
return [];
|
||||
return (array) $this->inspect()?->getConfig()?->getLabels();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNetworkNames(): array
|
||||
{
|
||||
// TODO: Implement getNetworkNames() method.
|
||||
return [];
|
||||
$networks = (array) $this->inspect()?->getNetworkSettings()?->getNetworks();
|
||||
return array_keys($networks);
|
||||
}
|
||||
|
||||
public function getNetworkId(string $networkName): string
|
||||
{
|
||||
// TODO: Implement getNetworkId() method.
|
||||
return '';
|
||||
$networks = (array) $this->inspect()?->getNetworkSettings()?->getNetworks();
|
||||
/** @var EndpointSettings | null $endpointSettings */
|
||||
$endpointSettings = $networks[$networkName] ?? null;
|
||||
$networkID = $endpointSettings?->getNetworkID();
|
||||
if ($networkID !== null) {
|
||||
return $networkID;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Network with name ‘{$networkName}’ does not exist");
|
||||
}
|
||||
|
||||
public function getIpAddress(string $networkName): string
|
||||
{
|
||||
// TODO: Implement getIpAddress() method.
|
||||
return '';
|
||||
$networks = (array) $this->inspect()?->getNetworkSettings()?->getNetworks();
|
||||
/** @var EndpointSettings | null $endpointSettings */
|
||||
$endpointSettings = $networks[$networkName] ?? null;
|
||||
$ipAddress = $endpointSettings?->getIPAddress();
|
||||
if ($ipAddress !== null) {
|
||||
return $ipAddress;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Network with name ‘{$networkName}’ does not exist");
|
||||
}
|
||||
|
||||
protected function inspect(): ContainersIdJsonGetResponse200 | null
|
||||
{
|
||||
if ($this->inspectResponse === null) {
|
||||
/** @var ContainersIdJsonGetResponse200 | null $inspectResponse */
|
||||
$inspectResponse = $this->dockerClient->containerInspect($this->id);
|
||||
$this->inspectResponse = $inspectResponse;
|
||||
}
|
||||
|
||||
return $this->inspectResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<PortBinding>>
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function ports(): iterable
|
||||
{
|
||||
$ports = $this->inspect()?->getNetworkSettings()?->getPorts();
|
||||
|
||||
if ($ports === null) {
|
||||
throw new RuntimeException('Failed to get ports from container');
|
||||
}
|
||||
|
||||
return $ports;
|
||||
}
|
||||
|
||||
protected function sanitizeOutput(string $output): string
|
||||
{
|
||||
return preg_replace('/[\x00-\x1F\x7F]/u', '', $output) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ interface StartedTestContainer
|
||||
|
||||
public function getIpAddress(string $networkName): string;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getLabels(): array;
|
||||
|
||||
public function logs(): string;
|
||||
@@ -35,6 +38,9 @@ interface StartedTestContainer
|
||||
|
||||
public function getNetworkId(string $networkName): string;
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNetworkNames(): array;
|
||||
|
||||
public function restart(): self;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Utils;
|
||||
|
||||
use Docker\API\Model\Network;
|
||||
use Docker\Docker;
|
||||
use RuntimeException;
|
||||
use Testcontainers\Container\GenericContainer;
|
||||
use Testcontainers\ContainerClient\DockerContainerClient;
|
||||
|
||||
class HostResolver
|
||||
{
|
||||
public function __construct(protected ?Docker $dockerClient = null)
|
||||
{
|
||||
$this->dockerClient = $dockerClient ?? DockerContainerClient::getDockerClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the host address for connecting to a container.
|
||||
*
|
||||
* The resolution process is as follows:
|
||||
* 1. If user overrides are allowed and TESTCONTAINERS_HOST_OVERRIDE is set, its value is returned.
|
||||
* 2. Otherwise, the DOCKER_HOST environment variable is parsed.
|
||||
* - If the scheme is one of http, https, or tcp, the hostname is used.
|
||||
* - If the scheme is unix or npipe and the process is running in a container, the network gateway
|
||||
* is determined by inspecting the relevant Docker network or running a temporary container.
|
||||
* 3. If no other value can be determined, "localhost" is returned.
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException If the DOCKER_HOST scheme is unsupported.
|
||||
*/
|
||||
public function resolveHost(): string
|
||||
{
|
||||
if ($this->allowUserOverrides() && ($override = getenv('TESTCONTAINERS_HOST_OVERRIDE')) !== false) {
|
||||
return $override;
|
||||
}
|
||||
|
||||
// Get DOCKER_HOST URI, defaulting to a TCP endpoint if not set.
|
||||
$dockerHostUri = getenv('DOCKER_HOST') ?: 'tcp://127.0.0.1:2375';
|
||||
$parts = parse_url($dockerHostUri);
|
||||
if ($parts === false || !isset($parts['scheme'])) {
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
$scheme = $parts['scheme'];
|
||||
|
||||
switch ($scheme) {
|
||||
case 'http':
|
||||
case 'https':
|
||||
case 'tcp':
|
||||
return $parts['host'] ?? 'localhost';
|
||||
|
||||
case 'unix':
|
||||
case 'npipe':
|
||||
if ($this->isInContainer()) {
|
||||
// If using podman, choose "podman" network; otherwise, use "bridge"
|
||||
$networkName = (str_contains($dockerHostUri, 'podman.sock')) ? 'podman' : 'bridge';
|
||||
if ($gateway = $this->findGateway($networkName)) {
|
||||
return $gateway;
|
||||
}
|
||||
if ($defaultGateway = $this->findDefaultGateway()) {
|
||||
return $defaultGateway;
|
||||
}
|
||||
}
|
||||
return 'localhost';
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unsupported Docker host scheme: {$scheme}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function allowUserOverrides(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the code is running inside a container.
|
||||
*/
|
||||
protected function isInContainer(): bool
|
||||
{
|
||||
return file_exists('/.dockerenv');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects the given network and returns its gateway IP address if found.
|
||||
*
|
||||
* @param string $networkName
|
||||
* @return string|null
|
||||
*/
|
||||
protected function findGateway(string $networkName): ?string
|
||||
{
|
||||
try {
|
||||
/** @var Network|null $networkInspect */
|
||||
$networkInspect = $this->dockerClient?->networkInspect($networkName);
|
||||
$ipamConfig = $networkInspect?->getIPAM()?->getConfig();
|
||||
if ($ipamConfig !== null) {
|
||||
foreach ($ipamConfig as $config) {
|
||||
if ($config->getGateway() !== null) {
|
||||
return $config->getGateway();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a temporary container to determine the default gateway.
|
||||
*/
|
||||
protected function findDefaultGateway(): ?string
|
||||
{
|
||||
$tmpContainer = null;
|
||||
try {
|
||||
// Create a temporary container using a lightweight Alpine image.
|
||||
$tmpContainer = (new GenericContainer('alpine:3.14'))
|
||||
->withCommand(['tail', '-f', '/dev/null'])
|
||||
->start();
|
||||
$result = $tmpContainer->exec(['sh', '-c', "ip route | awk '/default/ { print $3 }'"]);
|
||||
$tmpContainer->stop();
|
||||
return $result;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
} finally {
|
||||
if ($tmpContainer !== null) {
|
||||
try {
|
||||
$tmpContainer->stop();
|
||||
} catch (\Throwable) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ abstract class ContainerTestCase extends TestCase
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->container->stop();
|
||||
if (isset($this->container)) {
|
||||
$this->container->stop();
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Tests\Integration;
|
||||
|
||||
use Testcontainers\Container\GenericContainer;
|
||||
|
||||
class StartedGenericContainerTest extends ContainerTestCase
|
||||
{
|
||||
public function testShouldReturnContainerId(): void
|
||||
{
|
||||
$container = (new GenericContainer('alpine'))
|
||||
->withCommand(['tail', '-f', '/dev/null'])
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
self::assertNotEmpty($container->getId(), 'Container ID should not be empty');
|
||||
}
|
||||
|
||||
public function testShouldReturnLastExecId(): void
|
||||
{
|
||||
$container = (new GenericContainer('alpine'))
|
||||
->withCommand(['tail', '-f', '/dev/null'])
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$container->exec(['echo', 'Test Exec ID']);
|
||||
|
||||
$lastExecId = $container->getLastExecId();
|
||||
|
||||
self::assertNotNull($lastExecId, 'Last exec ID should not be null');
|
||||
self::assertNotEmpty($lastExecId, 'Last exec ID should not be empty');
|
||||
self::assertMatchesRegularExpression('/^[0-9a-f]+$/', $lastExecId, 'Last exec ID should be a valid hexadecimal string');
|
||||
}
|
||||
|
||||
public function testShouldExecuteCommandInContainer(): void
|
||||
{
|
||||
$container = (new GenericContainer('alpine'))
|
||||
->withCommand(['tail', '-f', '/dev/null'])
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$output = $container->exec(['echo', 'Hello, Testcontainers!']);
|
||||
self::assertSame('Hello, Testcontainers!', $output);
|
||||
}
|
||||
|
||||
public function testShouldStopContainer(): void
|
||||
{
|
||||
$container = (new GenericContainer('alpine'))
|
||||
->withCommand(['tail', '-f', '/dev/null'])
|
||||
->start();
|
||||
|
||||
self::assertNotEmpty($container->getId(), 'Container ID should not be empty');
|
||||
|
||||
$stoppedContainer = $container->stop();
|
||||
|
||||
self::assertNotNull($stoppedContainer, 'Stopped container should not be null');
|
||||
self::assertSame(
|
||||
$container->getId(),
|
||||
$stoppedContainer->getId(),
|
||||
'Stopped container ID should match the original container ID'
|
||||
);
|
||||
|
||||
self::assertStringContainsString(
|
||||
'No such container',
|
||||
$container->logs(),
|
||||
'Expected message indicating container does not exist'
|
||||
);
|
||||
}
|
||||
|
||||
public function testShouldRestartContainer(): void
|
||||
{
|
||||
$container = (new GenericContainer('nginx'))
|
||||
->withExposedPorts(80)
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$containerIdBeforeRestart = $container->getId();
|
||||
$container->restart();
|
||||
$containerIdAfterRestart = $container->getId();
|
||||
|
||||
self::assertSame(
|
||||
$containerIdBeforeRestart,
|
||||
$containerIdAfterRestart,
|
||||
'Container ID should remain the same after restart'
|
||||
);
|
||||
}
|
||||
|
||||
public function testShouldRetrieveLogs(): void
|
||||
{
|
||||
$container = (new GenericContainer('alpine'))
|
||||
->withCommand(['sh', '-c', 'echo "Hello from logs!" && tail -f /dev/null'])
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$logs = $container->logs();
|
||||
self::assertStringContainsString('Hello from logs!', $logs);
|
||||
}
|
||||
|
||||
public function testShouldRetrieveHost(): void
|
||||
{
|
||||
$container = (new GenericContainer('alpine'))
|
||||
->withCommand(['tail', '-f', '/dev/null'])
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$host = $container->getHost();
|
||||
self::assertSame('127.0.0.1', $host, 'Host should be 127.0.0.1');
|
||||
}
|
||||
|
||||
public function testShouldRetrieveFirstMappedPort(): void
|
||||
{
|
||||
$container = (new GenericContainer('nginx'))
|
||||
->withExposedPorts(80)
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$mappedPort = $container->getFirstMappedPort();
|
||||
self::assertGreaterThan(0, $mappedPort, 'Mapped port should be greater than 0');
|
||||
}
|
||||
|
||||
public function testShouldRetrieveMappedPort(): void
|
||||
{
|
||||
$container = (new GenericContainer('nginx'))
|
||||
->withExposedPorts(80)
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$mappedPort = $container->getMappedPort(80);
|
||||
self::assertGreaterThan(0, $mappedPort, 'Mapped port for 80 should be greater than 0');
|
||||
}
|
||||
|
||||
public function testShouldRetrieveContainerName(): void
|
||||
{
|
||||
$name = 'test-container-name';
|
||||
$container = (new GenericContainer('alpine'))
|
||||
->withName($name)
|
||||
->withCommand(['tail', '-f', '/dev/null'])
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
self::assertSame($name, $container->getName(), 'Container name should match');
|
||||
}
|
||||
|
||||
public function testShouldRetrieveLabels(): void
|
||||
{
|
||||
$labels = [
|
||||
'label-1' => 'value-1',
|
||||
'label-2' => 'value-2',
|
||||
];
|
||||
|
||||
$container = (new GenericContainer('alpine'))
|
||||
->withLabels($labels)
|
||||
->withCommand(['tail', '-f', '/dev/null'])
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$retrievedLabels = $container->getLabels();
|
||||
|
||||
self::assertArrayHasKey('label-1', $retrievedLabels);
|
||||
self::assertSame('value-1', $retrievedLabels['label-1']);
|
||||
self::assertArrayHasKey('label-2', $retrievedLabels);
|
||||
self::assertSame('value-2', $retrievedLabels['label-2']);
|
||||
}
|
||||
|
||||
public function testShouldRetrieveNetworkNames(): void
|
||||
{
|
||||
$container = (new GenericContainer('nginx'))
|
||||
->withExposedPorts(80)
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$networks = $container->getNetworkNames();
|
||||
|
||||
self::assertNotEmpty($networks, 'Networks should not be empty');
|
||||
}
|
||||
|
||||
public function testShouldRetrieveIpAddressFromNetwork(): void
|
||||
{
|
||||
$container = (new GenericContainer('nginx'))
|
||||
->withExposedPorts(80)
|
||||
->start();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
$networks = $container->getNetworkNames();
|
||||
$networkName = $networks[0] ?? null;
|
||||
|
||||
self::assertNotNull($networkName, 'Network name should not be null');
|
||||
|
||||
$ipAddress = $container->getIpAddress($networkName);
|
||||
|
||||
self::assertNotEmpty($ipAddress, 'IP address should not be empty');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainers\Tests\Unit\Utils;
|
||||
|
||||
use Docker\Docker;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
use Testcontainers\Utils\HostResolver;
|
||||
|
||||
class HostResolverTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
putenv('TESTCONTAINERS_HOST_OVERRIDE');
|
||||
putenv('DOCKER_HOST');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
putenv('TESTCONTAINERS_HOST_OVERRIDE');
|
||||
putenv('DOCKER_HOST');
|
||||
}
|
||||
|
||||
public function testReturnsTestcontainersHostOverrideFromEnvironment(): void
|
||||
{
|
||||
// When the override is set, it should be returned.
|
||||
putenv('TESTCONTAINERS_HOST_OVERRIDE=tcp://another:2375');
|
||||
putenv('DOCKER_HOST=tcp://docker:2375');
|
||||
|
||||
$dummyClient = $this->createMock(Docker::class);
|
||||
$resolver = new HostResolver($dummyClient);
|
||||
$host = $resolver->resolveHost();
|
||||
$this->assertEquals('tcp://another:2375', $host);
|
||||
}
|
||||
|
||||
public function testReturnsHostnameForTcpProtocols(): void
|
||||
{
|
||||
$protocols = ['tcp', 'http', 'https'];
|
||||
foreach ($protocols as $protocol) {
|
||||
putenv('DOCKER_HOST=' . $protocol . '://docker:2375');
|
||||
// Clear any override.
|
||||
putenv('TESTCONTAINERS_HOST_OVERRIDE');
|
||||
$dummyClient = $this->createMock(Docker::class);
|
||||
$resolver = new HostResolver($dummyClient);
|
||||
$host = $resolver->resolveHost();
|
||||
$this->assertEquals('docker', $host, "Protocol {$protocol} did not return expected hostname.");
|
||||
}
|
||||
}
|
||||
|
||||
public function testDoesNotReturnOverrideWhenAllowUserOverridesIsFalse(): void
|
||||
{
|
||||
$dummyClient = $this->createMock(Docker::class);
|
||||
$resolver = new class ($dummyClient) extends HostResolver {
|
||||
protected function allowUserOverrides(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
putenv('TESTCONTAINERS_HOST_OVERRIDE=tcp://another:2375');
|
||||
putenv('DOCKER_HOST=tcp://docker:2375');
|
||||
$host = $resolver->resolveHost();
|
||||
$this->assertEquals('docker', $host);
|
||||
}
|
||||
|
||||
public function testReturnsLocalhostForUnixAndNpipeProtocolsWhenNotInContainer(): void
|
||||
{
|
||||
$dummyClient = $this->createMock(Docker::class);
|
||||
$resolver = new class ($dummyClient) extends HostResolver {
|
||||
protected function isInContainer(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
foreach (['unix://docker:2375', 'npipe://docker:2375'] as $uri) {
|
||||
putenv('DOCKER_HOST=' . $uri);
|
||||
putenv('TESTCONTAINERS_HOST_OVERRIDE');
|
||||
$host = $resolver->resolveHost();
|
||||
$this->assertEquals('localhost', $host, "URI {$uri} should return 'localhost' when not in a container.");
|
||||
}
|
||||
}
|
||||
|
||||
public function testReturnsHostFromGatewayWhenRunningInContainer(): void
|
||||
{
|
||||
// For this test we simulate that we are in a container and the Docker client returns a gateway.
|
||||
$dockerClient = $this->getMockBuilder(Docker::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
// Build a fake network inspection response:
|
||||
$fakeConfig = new class () {
|
||||
public function getGateway(): ?string
|
||||
{
|
||||
return '172.0.0.1';
|
||||
}
|
||||
};
|
||||
$fakeIPAM = new class ($fakeConfig) {
|
||||
/** @var object[] */
|
||||
private array $config;
|
||||
public function __construct(object $config)
|
||||
{
|
||||
$this->config = [$config];
|
||||
}
|
||||
/** @return object[] */
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
};
|
||||
$fakeNetwork = new class ($fakeIPAM) {
|
||||
private object $ipam;
|
||||
public function __construct(object $ipam)
|
||||
{
|
||||
$this->ipam = $ipam;
|
||||
}
|
||||
public function getIPAM(): object
|
||||
{
|
||||
return $this->ipam;
|
||||
}
|
||||
};
|
||||
|
||||
// Expect that networkInspect will be called with "bridge" (since DOCKER_HOST does not contain "podman.sock")
|
||||
$dockerClient->expects($this->once())
|
||||
->method('networkInspect')
|
||||
->with($this->equalTo('bridge'))
|
||||
->willReturn($fakeNetwork);
|
||||
|
||||
// Override isInContainer() to simulate being inside a container.
|
||||
$resolver = new class ($dockerClient) extends HostResolver {
|
||||
protected function isInContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
putenv('DOCKER_HOST=unix://docker:2375');
|
||||
putenv('TESTCONTAINERS_HOST_OVERRIDE');
|
||||
$host = $resolver->resolveHost();
|
||||
$this->assertEquals('172.0.0.1', $host);
|
||||
}
|
||||
|
||||
public function testUsesBridgeNetworkAsGatewayForDockerProvider(): void
|
||||
{
|
||||
// For Docker provider (non-Podman) the network used should be "bridge".
|
||||
$dockerClient = $this->getMockBuilder(Docker::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
// Expect networkInspect to be called with "bridge"
|
||||
$dockerClient->expects($this->once())
|
||||
->method('networkInspect')
|
||||
->with($this->equalTo('bridge'))
|
||||
->willReturn(null); // Simulate not finding a gateway
|
||||
|
||||
$resolver = new class ($dockerClient) extends HostResolver {
|
||||
protected function isInContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
putenv('DOCKER_HOST=unix://docker:2375');
|
||||
$host = $resolver->resolveHost();
|
||||
// Since no gateway is found, fallback is "localhost"
|
||||
$this->assertEquals('localhost', $host);
|
||||
}
|
||||
|
||||
public function testUsesPodmanNetworkAsGatewayForPodmanProvider(): void
|
||||
{
|
||||
// For Podman, DOCKER_HOST contains "podman.sock" so the network should be "podman".
|
||||
$dockerClient = $this->getMockBuilder(Docker::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
// Expect networkInspect to be called with "podman"
|
||||
$dockerClient->expects($this->once())
|
||||
->method('networkInspect')
|
||||
->with($this->equalTo('podman'))
|
||||
->willReturn(null); // Simulate not finding a gateway
|
||||
|
||||
$resolver = new class ($dockerClient) extends HostResolver {
|
||||
protected function isInContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
putenv('DOCKER_HOST=unix://podman.sock');
|
||||
$host = $resolver->resolveHost();
|
||||
$this->assertEquals('localhost', $host);
|
||||
}
|
||||
|
||||
public function testReturnsHostFromDefaultGatewayWhenRunningInContainer(): void
|
||||
{
|
||||
// Override both findGateway() and findDefaultGateway() to simulate a missing network gateway and a default gateway result.
|
||||
$dummyClient = $this->createMock(Docker::class);
|
||||
$resolver = new class ($dummyClient) extends HostResolver {
|
||||
protected function isInContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
protected function findGateway(string $networkName): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
protected function findDefaultGateway(): ?string
|
||||
{
|
||||
return '172.0.0.2';
|
||||
}
|
||||
};
|
||||
|
||||
putenv('DOCKER_HOST=unix://docker:2375');
|
||||
$host = $resolver->resolveHost();
|
||||
$this->assertEquals('172.0.0.2', $host);
|
||||
}
|
||||
|
||||
public function testReturnsLocalhostIfUnableToFindGateway(): void
|
||||
{
|
||||
// Override to simulate that neither network inspection nor default gateway yield a result.
|
||||
$dummyClient = $this->createMock(Docker::class);
|
||||
$resolver = new class ($dummyClient) extends HostResolver {
|
||||
protected function isInContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
protected function findGateway(string $networkName): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
protected function findDefaultGateway(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
putenv('DOCKER_HOST=unix://docker:2375');
|
||||
$host = $resolver->resolveHost();
|
||||
$this->assertEquals('localhost', $host);
|
||||
}
|
||||
|
||||
public function testThrowsForUnsupportedProtocol(): void
|
||||
{
|
||||
putenv('DOCKER_HOST=invalid://unknown');
|
||||
$dummyClient = $this->createMock(Docker::class);
|
||||
$resolver = new HostResolver($dummyClient);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage("Unsupported Docker host scheme: invalid");
|
||||
|
||||
$resolver->resolveHost();
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ class TarBuilderTest extends TestCase
|
||||
mkdir($extractDir);
|
||||
$this->extractTar($tarPath, $extractDir);
|
||||
|
||||
$scanned = array_diff(scandir($extractDir), ['.', '..']);
|
||||
$scanned = array_diff(scandir($extractDir) ?: [], ['.', '..']);
|
||||
$this->assertCount(0, $scanned, 'Expected empty directory');
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ class TarBuilderTest extends TestCase
|
||||
mkdir($extractDir);
|
||||
$this->extractTar($tarPath, $extractDir);
|
||||
|
||||
$scanned = array_diff(scandir($extractDir), ['.', '..']);
|
||||
$scanned = array_diff(scandir($extractDir) ?: [], ['.', '..']);
|
||||
$this->assertCount(0, $scanned, 'Expected no files after clear()');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user