mirror of
https://github.com/stan220/testcontainers-php.git
synced 2026-09-12 11:29:36 +00:00
Merge pull request #26 from rw4lll/feat/docker-engine-api-client
Added withCopy*() functionality. Improved tests.
This commit is contained in:
+3
-2
@@ -23,7 +23,7 @@
|
|||||||
"ext-pdo_mysql": "*",
|
"ext-pdo_mysql": "*",
|
||||||
"ext-pdo_pgsql": "*",
|
"ext-pdo_pgsql": "*",
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"brianium/paratest": "^6.6",
|
"brianium/paratest": "^6.11",
|
||||||
"friendsofphp/php-cs-fixer": "^3.12",
|
"friendsofphp/php-cs-fixer": "^3.12",
|
||||||
"phpstan/phpstan": "^1.8",
|
"phpstan/phpstan": "^1.8",
|
||||||
"phpstan/phpstan-phpunit": "^1.1",
|
"phpstan/phpstan-phpunit": "^1.1",
|
||||||
@@ -41,7 +41,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"integration": "paratest tests/ --bootstrap vendor/autoload.php -f",
|
"integration": "paratest tests/ --exclude-group=legacy --bootstrap vendor/autoload.php -f",
|
||||||
|
"integration:old": "phpunit tests/Integration/OldTests --bootstrap vendor/autoload.php",
|
||||||
"cs": "php-cs-fixer fix --dry-run",
|
"cs": "php-cs-fixer fix --dry-run",
|
||||||
"cs:fix": "php-cs-fixer fix",
|
"cs:fix": "php-cs-fixer fix",
|
||||||
"phpstan": "phpstan analyse"
|
"phpstan": "phpstan analyse"
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ use Docker\API\Model\PortBinding;
|
|||||||
use Docker\Docker;
|
use Docker\Docker;
|
||||||
use Docker\Stream\CreateImageStream;
|
use Docker\Stream\CreateImageStream;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
use Testcontainers\ContainerClient\DockerContainerClient;
|
use Testcontainers\ContainerClient\DockerContainerClient;
|
||||||
use Testcontainers\Utils\PortGenerator\PortGenerator;
|
use Testcontainers\Utils\PortGenerator\PortGenerator;
|
||||||
use Testcontainers\Utils\PortGenerator\RandomUniquePortGenerator;
|
use Testcontainers\Utils\PortGenerator\RandomUniquePortGenerator;
|
||||||
use Testcontainers\Utils\PortNormalizer;
|
use Testcontainers\Utils\PortNormalizer;
|
||||||
|
use Testcontainers\Utils\TarBuilder;
|
||||||
use Testcontainers\Wait\WaitForContainer;
|
use Testcontainers\Wait\WaitForContainer;
|
||||||
use Testcontainers\Wait\WaitStrategy;
|
use Testcontainers\Wait\WaitStrategy;
|
||||||
|
|
||||||
@@ -58,8 +60,28 @@ class GenericContainer implements TestContainer
|
|||||||
protected PortGenerator $portGenerator;
|
protected PortGenerator $portGenerator;
|
||||||
|
|
||||||
protected bool $isPrivileged = false;
|
protected bool $isPrivileged = false;
|
||||||
|
|
||||||
protected ?string $networkName = null;
|
protected ?string $networkName = null;
|
||||||
|
|
||||||
|
protected ?string $user = null;
|
||||||
|
|
||||||
|
protected ?string $workingDir = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<array{source: string, target: string, mode?: int}>
|
||||||
|
*/
|
||||||
|
protected array $filesToCopy = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<array{source: string, target: string, mode?: int}>
|
||||||
|
*/
|
||||||
|
protected array $directoriesToCopy = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<array{content: string, target: string, mode?: int}>
|
||||||
|
*/
|
||||||
|
protected array $contentsToCopy = [];
|
||||||
|
|
||||||
protected int $startAttempts = 0;
|
protected int $startAttempts = 0;
|
||||||
protected const MAX_START_ATTEMPTS = 2;
|
protected const MAX_START_ATTEMPTS = 2;
|
||||||
|
|
||||||
@@ -94,6 +116,36 @@ class GenericContainer implements TestContainer
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array{source: string, target: string, mode?: int}> $files
|
||||||
|
*/
|
||||||
|
public function withCopyFilesToContainer(array $files): static
|
||||||
|
{
|
||||||
|
$this->filesToCopy = array_merge($this->filesToCopy, $files);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array{source: string, target: string, mode?: int}> $directories
|
||||||
|
*/
|
||||||
|
public function withCopyDirectoriesToContainer(array $directories): static
|
||||||
|
{
|
||||||
|
$this->directoriesToCopy = array_merge($this->directoriesToCopy, $directories);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array{content: string, target: string, mode?: int}> $contents
|
||||||
|
*/
|
||||||
|
public function withCopyContentToContainer(array $contents): static
|
||||||
|
{
|
||||||
|
$this->contentsToCopy = array_merge($this->contentsToCopy, $contents);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function withEntryPoint(string $entryPoint): static
|
public function withEntryPoint(string $entryPoint): static
|
||||||
{
|
{
|
||||||
$this->entryPoint = $entryPoint;
|
$this->entryPoint = $entryPoint;
|
||||||
@@ -230,6 +282,20 @@ class GenericContainer implements TestContainer
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function withUser(string $user): static
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withWorkingDir(string $workingDir): static
|
||||||
|
{
|
||||||
|
$this->workingDir = $workingDir;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function start(): StartedGenericContainer
|
public function start(): StartedGenericContainer
|
||||||
{
|
{
|
||||||
$this->startAttempts++;
|
$this->startAttempts++;
|
||||||
@@ -244,7 +310,7 @@ class GenericContainer implements TestContainer
|
|||||||
$this->id = $containerCreateResponse?->getId() ?? '';
|
$this->id = $containerCreateResponse?->getId() ?? '';
|
||||||
} catch (ContainerCreateNotFoundException) {
|
} catch (ContainerCreateNotFoundException) {
|
||||||
if ($this->startAttempts >= self::MAX_START_ATTEMPTS) {
|
if ($this->startAttempts >= self::MAX_START_ATTEMPTS) {
|
||||||
throw new \RuntimeException("Failed to start container after pulling image.");
|
throw new RuntimeException("Failed to start container after pulling image.");
|
||||||
}
|
}
|
||||||
// If the image is not found, pull it and try again
|
// If the image is not found, pull it and try again
|
||||||
// TODO: add withPullPolicy support
|
// TODO: add withPullPolicy support
|
||||||
@@ -254,12 +320,84 @@ class GenericContainer implements TestContainer
|
|||||||
|
|
||||||
$this->dockerClient->containerStart($this->id);
|
$this->dockerClient->containerStart($this->id);
|
||||||
|
|
||||||
|
if ($this->filesToCopy !== [] || $this->directoriesToCopy !== [] || $this->contentsToCopy !== []) {
|
||||||
|
$this->copyToContainer();
|
||||||
|
}
|
||||||
|
|
||||||
$startedContainer = new StartedGenericContainer($this->id);
|
$startedContainer = new StartedGenericContainer($this->id);
|
||||||
$this->waitStrategy->wait($startedContainer);
|
$this->waitStrategy->wait($startedContainer);
|
||||||
|
|
||||||
return $startedContainer;
|
return $startedContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a tar archive containing files/directories/content to the container,
|
||||||
|
* extracting it into a chosen directory (`$containerPath`). Allows setting
|
||||||
|
* Docker's `noOverwriteDirNonDir` and `copyUIDGID` query parameters.
|
||||||
|
*
|
||||||
|
* @param string $containerPath Path within the container to extract the tar contents. Must be a directory in the container.
|
||||||
|
* @param bool $noOverwriteDirNonDir If true, Docker will error if it would replace an existing directory with a non-directory and vice versa.
|
||||||
|
* @param bool $copyUIDGID If true, Docker will attempt to preserve UID/GID from the tar entries.
|
||||||
|
* @throws RuntimeException|InvalidArgumentException
|
||||||
|
*/
|
||||||
|
protected function copyToContainer(
|
||||||
|
string $containerPath = '/',
|
||||||
|
bool $noOverwriteDirNonDir = false,
|
||||||
|
bool $copyUIDGID = false
|
||||||
|
): void {
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
foreach ($this->filesToCopy as $file) {
|
||||||
|
$tarBuilder->addFile($file['source'], $file['target'], $file['mode'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->directoriesToCopy as $directory) {
|
||||||
|
$tarBuilder->addDirectory($directory['source'], $directory['target'], $directory['mode'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->contentsToCopy as $content) {
|
||||||
|
$tarBuilder->addContent($content['content'], $content['target'], $content['mode'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tarFilePath = $tarBuilder->buildTarArchive();
|
||||||
|
|
||||||
|
if (!is_file($tarFilePath)) {
|
||||||
|
throw new RuntimeException("Tar file does not exist at: $tarFilePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($tarFilePath, 'rb');
|
||||||
|
|
||||||
|
if ($handle === false) {
|
||||||
|
throw new RuntimeException("Cannot open temporary tar archive at: $tarFilePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryParams = [
|
||||||
|
'path' => $containerPath,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($noOverwriteDirNonDir) {
|
||||||
|
$queryParams['noOverwriteDirNonDir'] = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($copyUIDGID) {
|
||||||
|
$queryParams['copyUIDGID'] = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: should be improved. Currently without using dummy $result or FETCH_RESPONSE, the request is failing.
|
||||||
|
* Probably an issue with the beluga-php/docker-php client library.
|
||||||
|
* */
|
||||||
|
$result = $this->dockerClient->putContainerArchive(
|
||||||
|
$this->id,
|
||||||
|
$handle,
|
||||||
|
$queryParams,
|
||||||
|
$this->dockerClient::FETCH_RESPONSE
|
||||||
|
);
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
unlink($tarFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected function createContainerConfig(): ContainersCreatePostBody
|
protected function createContainerConfig(): ContainersCreatePostBody
|
||||||
{
|
{
|
||||||
$containerCreatePostBody = new ContainersCreatePostBody();
|
$containerCreatePostBody = new ContainersCreatePostBody();
|
||||||
@@ -267,6 +405,8 @@ class GenericContainer implements TestContainer
|
|||||||
$containerCreatePostBody->setCmd($this->command);
|
$containerCreatePostBody->setCmd($this->command);
|
||||||
$containerCreatePostBody->setLabels($this->labels);
|
$containerCreatePostBody->setLabels($this->labels);
|
||||||
$containerCreatePostBody->setHostname($this->hostname);
|
$containerCreatePostBody->setHostname($this->hostname);
|
||||||
|
$containerCreatePostBody->setWorkingDir($this->workingDir);
|
||||||
|
$containerCreatePostBody->setUser($this->user);
|
||||||
|
|
||||||
$envs = array_map(static fn ($key, $value) => "$key=$value", array_keys($this->env), $this->env);
|
$envs = array_map(static fn ($key, $value) => "$key=$value", array_keys($this->env), $this->env);
|
||||||
$containerCreatePostBody->setEnv($envs);
|
$containerCreatePostBody->setEnv($envs);
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainers\Utils;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
use RuntimeException;
|
||||||
|
use SplFileInfo;
|
||||||
|
|
||||||
|
class TarBuilder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<array{source: string, target: string, mode: int|null}>
|
||||||
|
*/
|
||||||
|
private array $files = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<array{source: string, target: string, mode: int|null}>
|
||||||
|
*/
|
||||||
|
private array $directories = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<array{content: string, target: string, mode: int|null}>
|
||||||
|
*/
|
||||||
|
private array $contents = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a single file from the local filesystem.
|
||||||
|
*/
|
||||||
|
public function addFile(string $source, string $target, ?int $mode = null): self
|
||||||
|
{
|
||||||
|
if (!is_file($source)) {
|
||||||
|
throw new InvalidArgumentException("Invalid file path: {$source}");
|
||||||
|
}
|
||||||
|
if (empty($target)) {
|
||||||
|
throw new InvalidArgumentException("Target path cannot be empty.");
|
||||||
|
}
|
||||||
|
if ($mode !== null && ($mode < 0 || $mode > 0o777)) {
|
||||||
|
throw new InvalidArgumentException("Invalid mode for file: {$mode}");
|
||||||
|
}
|
||||||
|
$this->files[] = [
|
||||||
|
'source' => $source,
|
||||||
|
'target' => $target,
|
||||||
|
'mode' => $mode,
|
||||||
|
];
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a directory (recursively) from the local filesystem.
|
||||||
|
*/
|
||||||
|
public function addDirectory(string $source, string $target, ?int $mode = null): self
|
||||||
|
{
|
||||||
|
$this->directories[] = [
|
||||||
|
'source' => $source,
|
||||||
|
'target' => $target,
|
||||||
|
'mode' => $mode,
|
||||||
|
];
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add inline string content that should become a file in the tar.
|
||||||
|
*/
|
||||||
|
public function addContent(string $content, string $target, ?int $mode = null): self
|
||||||
|
{
|
||||||
|
$this->contents[] = [
|
||||||
|
'content' => $content,
|
||||||
|
'target' => $target,
|
||||||
|
'mode' => $mode,
|
||||||
|
];
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the .tar archive from everything that was added (files, directories, contents).
|
||||||
|
*
|
||||||
|
* Returns the full path to the created .tar file.
|
||||||
|
*/
|
||||||
|
public function buildTarArchive(): string
|
||||||
|
{
|
||||||
|
$tempDir = $this->createTempDir();
|
||||||
|
|
||||||
|
$this->copyFilesToLocalDir($tempDir, $this->files);
|
||||||
|
$this->copyDirectoriesToLocalDir($tempDir, $this->directories);
|
||||||
|
$this->createFilesFromContent($tempDir, $this->contents);
|
||||||
|
|
||||||
|
$tarFilePath = $this->createTempTarPath();
|
||||||
|
$this->runTarCommand($tarFilePath, $tempDir);
|
||||||
|
$this->removeDirectoryRecursively($tempDir);
|
||||||
|
|
||||||
|
return $tarFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clear(): void
|
||||||
|
{
|
||||||
|
$this->files = [];
|
||||||
|
$this->directories = [];
|
||||||
|
$this->contents = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTempDir(): string
|
||||||
|
{
|
||||||
|
$tmpDirName = tempnam(sys_get_temp_dir(), 'tc_files_');
|
||||||
|
if ($tmpDirName === false) {
|
||||||
|
throw new RuntimeException("Failed to create a temp file for tar data");
|
||||||
|
}
|
||||||
|
// tempnam() creates a file; remove it and create directory instead
|
||||||
|
unlink($tmpDirName);
|
||||||
|
|
||||||
|
if (!mkdir($tmpDirName) && !is_dir($tmpDirName)) {
|
||||||
|
throw new RuntimeException("Failed to create temp directory: {$tmpDirName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tmpDirName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTempTarPath(): string
|
||||||
|
{
|
||||||
|
$tmpFile = tempnam(sys_get_temp_dir(), 'tc_tar_');
|
||||||
|
|
||||||
|
if ($tmpFile === false) {
|
||||||
|
throw new RuntimeException("Failed to create temp file for tar archive");
|
||||||
|
}
|
||||||
|
|
||||||
|
$tarFilePath = $tmpFile . '.tar';
|
||||||
|
|
||||||
|
if (!rename($tmpFile, $tarFilePath)) {
|
||||||
|
throw new RuntimeException("Failed renaming temp file to .tar");
|
||||||
|
}
|
||||||
|
return $tarFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function runTarCommand(string $tarFilePath, string $sourceDir): void
|
||||||
|
{
|
||||||
|
// 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',
|
||||||
|
escapeshellarg($tarFilePath),
|
||||||
|
escapeshellarg($sourceDir)
|
||||||
|
);
|
||||||
|
|
||||||
|
exec($cmd, $output, $exitCode);
|
||||||
|
|
||||||
|
if ($exitCode !== 0) {
|
||||||
|
$errorText = implode("\n", $output);
|
||||||
|
throw new RuntimeException("Failed to create tar archive:\n{$errorText}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeDirectoryRecursively(string $dir): void
|
||||||
|
{
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $item) {
|
||||||
|
if (!$item instanceof SplFileInfo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$path = $item->getRealPath();
|
||||||
|
if ($item->isDir()) {
|
||||||
|
rmdir($path);
|
||||||
|
} else {
|
||||||
|
unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rmdir($dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array{source: string, target: string, mode: int|null}> $files
|
||||||
|
*/
|
||||||
|
private function copyFilesToLocalDir(string $tempDir, array $files): void
|
||||||
|
{
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$source = $file['source'];
|
||||||
|
$target = $file['target'];
|
||||||
|
$mode = $file['mode'] ?? null;
|
||||||
|
|
||||||
|
if (!is_file($source)) {
|
||||||
|
throw new InvalidArgumentException("File not found: $source");
|
||||||
|
}
|
||||||
|
$destPath = $this->makeDestPath($tempDir, $target);
|
||||||
|
$this->ensureParentDir($destPath);
|
||||||
|
|
||||||
|
if (!copy($source, $destPath)) {
|
||||||
|
throw new RuntimeException("Failed to copy file $source to $destPath");
|
||||||
|
}
|
||||||
|
if ($mode !== null) {
|
||||||
|
chmod($destPath, $mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array{source: string, target: string, mode: int|null}> $directories
|
||||||
|
*/
|
||||||
|
private function copyDirectoriesToLocalDir(string $tempDir, array $directories): void
|
||||||
|
{
|
||||||
|
foreach ($directories as $dir) {
|
||||||
|
$source = $dir['source'];
|
||||||
|
$target = $dir['target'];
|
||||||
|
$mode = $dir['mode'] ?? null;
|
||||||
|
|
||||||
|
if (!is_dir($source)) {
|
||||||
|
throw new InvalidArgumentException("Directory not found: $source");
|
||||||
|
}
|
||||||
|
$destPath = $this->makeDestPath($tempDir, $target);
|
||||||
|
$this->copyDirectoryRecursively($source, $destPath);
|
||||||
|
|
||||||
|
if ($mode !== null) {
|
||||||
|
chmod($destPath, $mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array{content: string, target: string, mode: int|null}> $contents
|
||||||
|
*/
|
||||||
|
private function createFilesFromContent(string $tempDir, array $contents): void
|
||||||
|
{
|
||||||
|
foreach ($contents as $content) {
|
||||||
|
$data = $content['content'];
|
||||||
|
$target = $content['target'];
|
||||||
|
$mode = $content['mode'] ?? null;
|
||||||
|
|
||||||
|
$destPath = $this->makeDestPath($tempDir, $target);
|
||||||
|
$this->ensureParentDir($destPath);
|
||||||
|
|
||||||
|
file_put_contents($destPath, $data);
|
||||||
|
if ($mode !== null) {
|
||||||
|
chmod($destPath, $mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function copyDirectoryRecursively(string $sourceDir, string $destDir): void
|
||||||
|
{
|
||||||
|
$this->ensureParentDir($destDir);
|
||||||
|
|
||||||
|
$innerIterator = new RecursiveDirectoryIterator($sourceDir, \FilesystemIterator::SKIP_DOTS);
|
||||||
|
|
||||||
|
/** @var RecursiveIteratorIterator<RecursiveDirectoryIterator> $iterator */
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
$innerIterator,
|
||||||
|
RecursiveIteratorIterator::SELF_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $item) {
|
||||||
|
if (!$item instanceof SplFileInfo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var RecursiveDirectoryIterator $innerIterator */
|
||||||
|
$innerIterator = $iterator->getInnerIterator();
|
||||||
|
$subPathName = $innerIterator->getSubPathName();
|
||||||
|
$targetPath = $destDir . '/' . $subPathName;
|
||||||
|
|
||||||
|
// Ensure the parent directory for the target path exists
|
||||||
|
$this->ensureParentDir($targetPath);
|
||||||
|
|
||||||
|
if ($item->isDir()) {
|
||||||
|
if (!mkdir($targetPath, 0o777, true) && !is_dir($targetPath)) {
|
||||||
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $targetPath));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
copy($item->getPathname(), $targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeDestPath(string $tempDir, string $target): string
|
||||||
|
{
|
||||||
|
return rtrim($tempDir, '/') . '/' . ltrim($target, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureParentDir(string $path): void
|
||||||
|
{
|
||||||
|
$parent = dirname($path);
|
||||||
|
if (!is_dir($parent) && !mkdir($parent, 0o777, true) && !is_dir($parent)) {
|
||||||
|
throw new RuntimeException("Failed to create parent directory: $parent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
hello world
|
||||||
@@ -9,10 +9,11 @@ use Testcontainers\Container\StartedTestContainer;
|
|||||||
|
|
||||||
abstract class ContainerTestCase extends TestCase
|
abstract class ContainerTestCase extends TestCase
|
||||||
{
|
{
|
||||||
protected static StartedTestContainer $container;
|
protected StartedTestContainer $container;
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
self::$container->stop();
|
$this->container->stop();
|
||||||
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,285 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Testcontainers\Tests\Integration;
|
namespace Testcontainers\Tests\Integration;
|
||||||
|
|
||||||
|
use Docker\API\Model\ContainersIdJsonGetResponse200;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RuntimeException;
|
||||||
use Testcontainers\Container\GenericContainer;
|
use Testcontainers\Container\GenericContainer;
|
||||||
|
use Testcontainers\Utils\PortGenerator\FixedPortGenerator;
|
||||||
|
use Testcontainers\Wait\WaitForHostPort;
|
||||||
|
|
||||||
class GenericContainerTest extends ContainerTestCase
|
class GenericContainerTest extends TestCase
|
||||||
{
|
{
|
||||||
public static function setUpBeforeClass(): void
|
|
||||||
{
|
|
||||||
self::$container = (new GenericContainer('alpine'))
|
|
||||||
->withCommand(['tail', '-f', '/dev/null'])
|
|
||||||
->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testExec(): void
|
public function testExec(): void
|
||||||
{
|
{
|
||||||
$actual = self::$container->exec(['echo', 'testcontainers']);
|
$container = (new GenericContainer('alpine'))
|
||||||
self::assertSame('testcontainers', $actual);
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->start();
|
||||||
|
$result = $container->exec(['echo', 'testcontainers']);
|
||||||
|
|
||||||
|
self::assertSame('testcontainers', $result);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldCopyContentToContainer(): void
|
||||||
|
{
|
||||||
|
$inlineContent = 'hello world';
|
||||||
|
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->withCopyContentToContainer([[
|
||||||
|
'content' => $inlineContent,
|
||||||
|
'target' => '/tmp/inline.txt',
|
||||||
|
]])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
$output = $container->exec(['cat', '/tmp/inline.txt']);
|
||||||
|
|
||||||
|
self::assertSame($inlineContent, $output);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldCopyDirectoryToContainer(): void
|
||||||
|
{
|
||||||
|
$testDir = sys_get_temp_dir() . '/copy-dir-test';
|
||||||
|
if (!is_dir($testDir)) {
|
||||||
|
mkdir($testDir);
|
||||||
|
}
|
||||||
|
file_put_contents($testDir . '/file1.txt', 'file1 contents');
|
||||||
|
file_put_contents($testDir . '/file2.txt', 'file2 contents');
|
||||||
|
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->withCopyDirectoriesToContainer([[
|
||||||
|
'source' => $testDir,
|
||||||
|
'target' => '/test-dir',
|
||||||
|
]])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
$output1 = $container->exec(['cat', '/test-dir/file1.txt']);
|
||||||
|
$output2 = $container->exec(['cat', '/test-dir/file2.txt']);
|
||||||
|
|
||||||
|
self::assertSame('file1 contents', $output1);
|
||||||
|
self::assertSame('file2 contents', $output2);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldCopyFileToContainer(): void
|
||||||
|
{
|
||||||
|
$localFilePath = sys_get_temp_dir() . '/copy-file-test.txt';
|
||||||
|
file_put_contents($localFilePath, 'hello from file');
|
||||||
|
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->withCopyFilesToContainer([[
|
||||||
|
'source' => $localFilePath,
|
||||||
|
'target' => '/tmp/test-file.txt',
|
||||||
|
]])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
$output = $container->exec(['cat', '/tmp/test-file.txt']);
|
||||||
|
|
||||||
|
self::assertSame('hello from file', $output);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldCopyFileWithPermissions(): void
|
||||||
|
{
|
||||||
|
$localFilePath = sys_get_temp_dir() . '/copy-perms-test.txt';
|
||||||
|
file_put_contents($localFilePath, 'check perms');
|
||||||
|
|
||||||
|
$mode = 0o777;
|
||||||
|
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->withCopyFilesToContainer([[
|
||||||
|
'source' => $localFilePath,
|
||||||
|
'target' => '/tmp/perm-file.txt',
|
||||||
|
'mode' => $mode,
|
||||||
|
]])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
$output = $container->exec(['stat', '-c', '%a', '/tmp/perm-file.txt']);
|
||||||
|
|
||||||
|
self::assertSame('777', trim($output));
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testShouldReturnFirstMappedPort(): void
|
||||||
|
{
|
||||||
|
$container = (new GenericContainer('nginx'))
|
||||||
|
->withPortGenerator(new FixedPortGenerator([8080]))
|
||||||
|
->withExposedPorts(80)
|
||||||
|
->withWait(new WaitForHostPort(8080))
|
||||||
|
->start();
|
||||||
|
$firstMappedPort = $container->getFirstMappedPort();
|
||||||
|
|
||||||
|
self::assertSame($firstMappedPort, 8080, 'First mapped port does not match 8080');
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetLabels(): void
|
||||||
|
{
|
||||||
|
$labels = [
|
||||||
|
'label-1' => 'value-1',
|
||||||
|
'label-2' => 'value-2',
|
||||||
|
];
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withLabels($labels)
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
/** @var ContainersIdJsonGetResponse200|null $inspectResult */
|
||||||
|
$inspectResult = $container->getClient()->containerInspect($container->getId());
|
||||||
|
$this->assertArrayHasKey('label-1', (array)$inspectResult?->getConfig()?->getLabels());
|
||||||
|
$this->assertSame('value-1', ((array)$inspectResult?->getConfig()?->getLabels())['label-1']);
|
||||||
|
$this->assertArrayHasKey('label-2', (array)$inspectResult?->getConfig()?->getLabels());
|
||||||
|
$this->assertSame('value-2', ((array)$inspectResult?->getConfig()?->getLabels())['label-2']);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetName(): void
|
||||||
|
{
|
||||||
|
$name = 'test-container-name';
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withName($name)
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
/** @var ContainersIdJsonGetResponse200|null $inspectResult */
|
||||||
|
$inspectResult = $container->getClient()->containerInspect($container->getId());
|
||||||
|
$this->assertSame('/'.$name, $inspectResult?->getName());
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetUser(): void
|
||||||
|
{
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withUser('nobody')
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
$output = $container->exec(['whoami']);
|
||||||
|
$this->assertStringContainsString('nobody', $output);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetWorkingDir(): void
|
||||||
|
{
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withWorkingDir('/tmp')
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
$output = $container->exec(['pwd']);
|
||||||
|
$this->assertStringContainsString('/tmp', $output);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldCaptureStderrWhenCommandFails(): void
|
||||||
|
{
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->start();
|
||||||
|
$result = $container->exec(['ls', '/nonexistent/path']);
|
||||||
|
|
||||||
|
self::assertStringContainsString('No such file or directory', $result, 'Expected stderr in the output');
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetEnvironmentVariables(): void
|
||||||
|
{
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->withEnvironment(['TEST_ENV' => 'testValue'])
|
||||||
|
->start();
|
||||||
|
$output = $container->exec(['env']);
|
||||||
|
|
||||||
|
self::assertStringContainsString('TEST_ENV=testValue', $output);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetHealthCheckCommand(): void
|
||||||
|
{
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->withHealthCheckCommand('echo "healthy" || exit 1')
|
||||||
|
->start();
|
||||||
|
|
||||||
|
/** @var ContainersIdJsonGetResponse200|null $inspectResult */
|
||||||
|
$inspectResult = $container->getClient()->containerInspect($container->getId());
|
||||||
|
$healthConfig = $inspectResult?->getConfig()?->getHealthcheck();
|
||||||
|
|
||||||
|
$this->assertNotNull($healthConfig);
|
||||||
|
$this->assertEquals(['CMD-SHELL', 'echo "healthy" || exit 1'], $healthConfig->getTest());
|
||||||
|
$this->assertSame(1000000000, $healthConfig->getInterval());
|
||||||
|
$this->assertSame(3000000000, $healthConfig->getTimeout());
|
||||||
|
$this->assertSame(3, $healthConfig->getRetries());
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetEntrypoint(): void
|
||||||
|
{
|
||||||
|
$container = (new GenericContainer('cristianrgreco/testcontainer:1.1.14'))
|
||||||
|
->withEntrypoint('node')
|
||||||
|
->withCommand(['index.js'])
|
||||||
|
->withExposedPorts(8080)
|
||||||
|
->start();
|
||||||
|
|
||||||
|
/** @var ContainersIdJsonGetResponse200|null $inspectResult */
|
||||||
|
$inspectResult = $container->getClient()->containerInspect($container->getId());
|
||||||
|
$entrypoint = $inspectResult?->getConfig()?->getEntrypoint() ?? [];
|
||||||
|
|
||||||
|
self::assertContains('node', $entrypoint);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetMount(): void
|
||||||
|
{
|
||||||
|
$localPath = __DIR__ . '/../Fixtures/Docker';
|
||||||
|
$containerPath = '/mnt/test-data';
|
||||||
|
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withMount($localPath, $containerPath)
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
$result = $container->exec(["cat", $containerPath.'/test.txt']);
|
||||||
|
self::assertSame('hello world', $result);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldSetPrivilegedMode(): void
|
||||||
|
{
|
||||||
|
$container = (new GenericContainer('alpine'))
|
||||||
|
->withPrivilegedMode()
|
||||||
|
->withCommand(['tail', '-f', '/dev/null'])
|
||||||
|
->start();
|
||||||
|
|
||||||
|
/** @var ContainersIdJsonGetResponse200|null $inspectResult */
|
||||||
|
$inspectResult = $container->getClient()->containerInspect($container->getId());
|
||||||
|
$privileged = $inspectResult?->getHostConfig()?->getPrivileged();
|
||||||
|
|
||||||
|
self::assertTrue($privileged);
|
||||||
|
|
||||||
|
$container->stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use Testcontainers\Modules\MariaDBContainer;
|
|||||||
|
|
||||||
class MariaDBContainerTest extends ContainerTestCase
|
class MariaDBContainerTest extends ContainerTestCase
|
||||||
{
|
{
|
||||||
public static function setUpBeforeClass(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
self::$container = (new MariaDBContainer())
|
$this->container = (new MariaDBContainer())
|
||||||
->withMariaDBDatabase('foo')
|
->withMariaDBDatabase('foo')
|
||||||
->withMariaDBUser('bar', 'baz')
|
->withMariaDBUser('bar', 'baz')
|
||||||
->start();
|
->start();
|
||||||
@@ -21,8 +21,8 @@ class MariaDBContainerTest extends ContainerTestCase
|
|||||||
$pdo = new \PDO(
|
$pdo = new \PDO(
|
||||||
sprintf(
|
sprintf(
|
||||||
'mysql:host=%s;port=%d',
|
'mysql:host=%s;port=%d',
|
||||||
self::$container->getHost(),
|
$this->container->getHost(),
|
||||||
self::$container->getFirstMappedPort()
|
$this->container->getFirstMappedPort()
|
||||||
),
|
),
|
||||||
'bar',
|
'bar',
|
||||||
'baz',
|
'baz',
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use Testcontainers\Modules\MySQLContainer;
|
|||||||
|
|
||||||
class MySQLContainerTest extends ContainerTestCase
|
class MySQLContainerTest extends ContainerTestCase
|
||||||
{
|
{
|
||||||
public static function setUpBeforeClass(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
self::$container = (new MySQLContainer())
|
$this->container = (new MySQLContainer())
|
||||||
->withMySQLDatabase('foo')
|
->withMySQLDatabase('foo')
|
||||||
->withMySQLUser('bar', 'baz')
|
->withMySQLUser('bar', 'baz')
|
||||||
->start();
|
->start();
|
||||||
@@ -21,8 +21,8 @@ class MySQLContainerTest extends ContainerTestCase
|
|||||||
$pdo = new \PDO(
|
$pdo = new \PDO(
|
||||||
sprintf(
|
sprintf(
|
||||||
'mysql:host=%s;port=%d',
|
'mysql:host=%s;port=%d',
|
||||||
self::$container->getHost(),
|
$this->container->getHost(),
|
||||||
self::$container->getFirstMappedPort()
|
$this->container->getFirstMappedPort()
|
||||||
),
|
),
|
||||||
'bar',
|
'bar',
|
||||||
'baz',
|
'baz',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use Testcontainers\Container\PostgresContainer;
|
|||||||
use Testcontainers\Container\RedisContainer;
|
use Testcontainers\Container\RedisContainer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @group legacy
|
||||||
* Old test classes kept to check backward compatibility
|
* Old test classes kept to check backward compatibility
|
||||||
*/
|
*/
|
||||||
class ContainerTest extends TestCase
|
class ContainerTest extends TestCase
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use Testcontainers\Wait\WaitForLog;
|
|||||||
use Testcontainers\Wait\WaitForTcpPortOpen;
|
use Testcontainers\Wait\WaitForTcpPortOpen;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @group legacy
|
||||||
* Old test classes kept to check backward compatibility
|
* Old test classes kept to check backward compatibility
|
||||||
*/
|
*/
|
||||||
class WaitStrategyTest extends TestCase
|
class WaitStrategyTest extends TestCase
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use Testcontainers\Modules\OpenSearchContainer;
|
|||||||
|
|
||||||
class OpenSearchContainerTest extends ContainerTestCase
|
class OpenSearchContainerTest extends ContainerTestCase
|
||||||
{
|
{
|
||||||
public static function setUpBeforeClass(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
self::$container = (new OpenSearchContainer())
|
$this->container = (new OpenSearchContainer())
|
||||||
->withDisabledSecurityPlugin()
|
->withDisabledSecurityPlugin()
|
||||||
->start();
|
->start();
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,8 @@ class OpenSearchContainerTest extends ContainerTestCase
|
|||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
curl_setopt($ch, CURLOPT_URL, sprintf(
|
curl_setopt($ch, CURLOPT_URL, sprintf(
|
||||||
'http://%s:%d',
|
'http://%s:%d',
|
||||||
self::$container->getHost(),
|
$this->container->getHost(),
|
||||||
self::$container->getFirstMappedPort()
|
$this->container->getFirstMappedPort()
|
||||||
));
|
));
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use Testcontainers\Modules\PostgresContainer;
|
|||||||
|
|
||||||
class PostgreSQLContainerTest extends ContainerTestCase
|
class PostgreSQLContainerTest extends ContainerTestCase
|
||||||
{
|
{
|
||||||
public static function setUpBeforeClass(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
self::$container = (new PostgresContainer())
|
$this->container = (new PostgresContainer())
|
||||||
->withPostgresUser('bar')
|
->withPostgresUser('bar')
|
||||||
->withPostgresDatabase('foo')
|
->withPostgresDatabase('foo')
|
||||||
->start();
|
->start();
|
||||||
@@ -21,8 +21,8 @@ class PostgreSQLContainerTest extends ContainerTestCase
|
|||||||
$pdo = new \PDO(
|
$pdo = new \PDO(
|
||||||
sprintf(
|
sprintf(
|
||||||
'pgsql:host=%s;port=%d;dbname=foo',
|
'pgsql:host=%s;port=%d;dbname=foo',
|
||||||
self::$container->getHost(),
|
$this->container->getHost(),
|
||||||
self::$container->getFirstMappedPort()
|
$this->container->getFirstMappedPort()
|
||||||
),
|
),
|
||||||
'bar',
|
'bar',
|
||||||
'test',
|
'test',
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ use Testcontainers\Modules\RedisContainer;
|
|||||||
|
|
||||||
class RedisContainerTest extends ContainerTestCase
|
class RedisContainerTest extends ContainerTestCase
|
||||||
{
|
{
|
||||||
public static function setUpBeforeClass(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
self::$container = (new RedisContainer())
|
$this->container = (new RedisContainer())
|
||||||
->start();
|
->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRedisContainer(): void
|
public function testRedisContainer(): void
|
||||||
{
|
{
|
||||||
$redisClient = new Client([
|
$redisClient = new Client([
|
||||||
'host' => self::$container->getHost(),
|
'host' => $this->container->getHost(),
|
||||||
'port' => self::$container->getFirstMappedPort(),
|
'port' => $this->container->getFirstMappedPort(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$redisClient->ping();
|
$redisClient->ping();
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainers\Tests\Unit\Utils;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
use RuntimeException;
|
||||||
|
use SplFileInfo;
|
||||||
|
use Testcontainers\Utils\TarBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \Testcontainers\Utils\TarBuilder
|
||||||
|
*/
|
||||||
|
class TarBuilderTest extends TestCase
|
||||||
|
{
|
||||||
|
private const TEST_CONTENT = 'hello world';
|
||||||
|
private string $tempDir;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->tempDir = sys_get_temp_dir() . '/tarbuilder_test_' . uniqid('', true);
|
||||||
|
mkdir($this->tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->removeDirectoryRecursively($this->tempDir);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldAddSingleFile(): void
|
||||||
|
{
|
||||||
|
$sourceFile = $this->tempDir . '/file.txt';
|
||||||
|
file_put_contents($sourceFile, self::TEST_CONTENT);
|
||||||
|
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
$tarBuilder->addFile($sourceFile, 'mydir/file_in_tar.txt', 0o644);
|
||||||
|
|
||||||
|
$tarPath = $tarBuilder->buildTarArchive();
|
||||||
|
|
||||||
|
$this->assertFileExists($tarPath, 'Tar file was not created');
|
||||||
|
|
||||||
|
$extractDir = $this->tempDir . '/extract';
|
||||||
|
mkdir($extractDir);
|
||||||
|
$this->extractTar($tarPath, $extractDir);
|
||||||
|
|
||||||
|
$extractedFile = $extractDir . '/mydir/file_in_tar.txt';
|
||||||
|
$this->assertFileExists($extractedFile);
|
||||||
|
$this->assertSame(self::TEST_CONTENT, file_get_contents($extractedFile));
|
||||||
|
|
||||||
|
$perms = substr(sprintf('%o', fileperms($extractedFile)), -3);
|
||||||
|
$this->assertSame('644', $perms, 'Expected file mode 0644');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldAddDirectoryRecursively(): void
|
||||||
|
{
|
||||||
|
$localDir = $this->tempDir . '/localdir';
|
||||||
|
mkdir($localDir);
|
||||||
|
file_put_contents($localDir . '/one.txt', 'file1');
|
||||||
|
file_put_contents($localDir . '/two.txt', 'file2');
|
||||||
|
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
$tarBuilder->addDirectory($localDir, 'mydir', 0o755);
|
||||||
|
$tarPath = $tarBuilder->buildTarArchive();
|
||||||
|
|
||||||
|
$this->assertFileExists($tarPath);
|
||||||
|
|
||||||
|
$extractDir = $this->tempDir . '/extractdir';
|
||||||
|
mkdir($extractDir);
|
||||||
|
$this->extractTar($tarPath, $extractDir);
|
||||||
|
|
||||||
|
$oneExtracted = $extractDir . '/mydir/one.txt';
|
||||||
|
$twoExtracted = $extractDir . '/mydir/two.txt';
|
||||||
|
$this->assertFileExists($oneExtracted);
|
||||||
|
$this->assertFileExists($twoExtracted);
|
||||||
|
|
||||||
|
$this->assertSame('file1', file_get_contents($oneExtracted));
|
||||||
|
$this->assertSame('file2', file_get_contents($twoExtracted));
|
||||||
|
|
||||||
|
$dirPerms = substr(sprintf('%o', fileperms($extractDir . '/mydir')), -3);
|
||||||
|
$this->assertSame('755', $dirPerms, 'Expected directory mode 0755');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldAddInlineContent(): void
|
||||||
|
{
|
||||||
|
$content = "Inline content test\nLine2";
|
||||||
|
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
$tarBuilder->addContent($content, 'some/path/inline.txt', 0o777);
|
||||||
|
$tarPath = $tarBuilder->buildTarArchive();
|
||||||
|
|
||||||
|
$this->assertFileExists($tarPath);
|
||||||
|
|
||||||
|
$extractDir = $this->tempDir . '/extractContent';
|
||||||
|
mkdir($extractDir);
|
||||||
|
$this->extractTar($tarPath, $extractDir);
|
||||||
|
|
||||||
|
$inlineExtracted = $extractDir . '/some/path/inline.txt';
|
||||||
|
$this->assertFileExists($inlineExtracted);
|
||||||
|
$this->assertSame($content, file_get_contents($inlineExtracted));
|
||||||
|
|
||||||
|
$perms = substr(sprintf('%o', fileperms($inlineExtracted)), -3);
|
||||||
|
$this->assertSame('777', $perms, 'Expected file mode 0777');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldFailOnInvalidFilePath(): void
|
||||||
|
{
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$tarBuilder->addFile('/some/nonexistent/file', 'target.txt');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldFailOnEmptyTarget(): void
|
||||||
|
{
|
||||||
|
$localFile = $this->tempDir . '/somefile.txt';
|
||||||
|
file_put_contents($localFile, 'abc');
|
||||||
|
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$tarBuilder->addFile($localFile, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldFailOnInvalidMode(): void
|
||||||
|
{
|
||||||
|
$localFile = $this->tempDir . '/somefile.txt';
|
||||||
|
file_put_contents($localFile, 'abc');
|
||||||
|
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$tarBuilder->addFile($localFile, 'target.txt', 9999);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldCreateEmptyTarIfNoItemsAdded(): void
|
||||||
|
{
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
|
||||||
|
$tarPath = $tarBuilder->buildTarArchive();
|
||||||
|
$this->assertFileExists($tarPath);
|
||||||
|
|
||||||
|
$extractDir = $this->tempDir . '/extractEmpty';
|
||||||
|
mkdir($extractDir);
|
||||||
|
$this->extractTar($tarPath, $extractDir);
|
||||||
|
|
||||||
|
$scanned = array_diff(scandir($extractDir), ['.', '..']);
|
||||||
|
$this->assertCount(0, $scanned, 'Expected empty directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldClearItems(): void
|
||||||
|
{
|
||||||
|
$tarBuilder = new TarBuilder();
|
||||||
|
$localFile = $this->tempDir . '/somefile.txt';
|
||||||
|
file_put_contents($localFile, 'abc');
|
||||||
|
$tarBuilder->addFile($localFile, 'test.txt');
|
||||||
|
|
||||||
|
$tarBuilder->clear();
|
||||||
|
|
||||||
|
$tarPath = $tarBuilder->buildTarArchive();
|
||||||
|
$this->assertFileExists($tarPath);
|
||||||
|
|
||||||
|
$extractDir = $this->tempDir . '/extractCleared';
|
||||||
|
mkdir($extractDir);
|
||||||
|
$this->extractTar($tarPath, $extractDir);
|
||||||
|
|
||||||
|
$scanned = array_diff(scandir($extractDir), ['.', '..']);
|
||||||
|
$this->assertCount(0, $scanned, 'Expected no files after clear()');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to extract a .tar for verification.
|
||||||
|
*/
|
||||||
|
private function extractTar(string $tarPath, string $destination): void
|
||||||
|
{
|
||||||
|
$cmd = sprintf(
|
||||||
|
'tar -xpf %s -C %s 2>&1',
|
||||||
|
escapeshellarg($tarPath),
|
||||||
|
escapeshellarg($destination)
|
||||||
|
);
|
||||||
|
|
||||||
|
exec($cmd, $output, $exitCode);
|
||||||
|
if ($exitCode !== 0) {
|
||||||
|
$errorText = implode("\n", $output);
|
||||||
|
throw new RuntimeException("Failed to extract tar:\n{$errorText}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively remove directory.
|
||||||
|
*/
|
||||||
|
private function removeDirectoryRecursively(string $path): void
|
||||||
|
{
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var RecursiveIteratorIterator<RecursiveDirectoryIterator> $items */
|
||||||
|
$items = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!$item instanceof SplFileInfo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($item->isDir()) {
|
||||||
|
rmdir($item->getRealPath());
|
||||||
|
} else {
|
||||||
|
unlink($item->getRealPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rmdir($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user