added withCopy* functionality to copy data into containers. Additional tests and improvements

This commit is contained in:
Sergei Shitikov
2025-01-19 23:12:19 +01:00
parent dfff7ef282
commit 53c67a15ed
4 changed files with 830 additions and 1 deletions
+175
View File
@@ -6,6 +6,7 @@ namespace Testcontainers\Tests\Integration;
use Docker\API\Model\ContainersIdJsonGetResponse200;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Testcontainers\Container\GenericContainer;
use Testcontainers\Utils\PortGenerator\FixedPortGenerator;
use Testcontainers\Wait\WaitForHostPort;
@@ -24,6 +25,94 @@ class GenericContainerTest extends TestCase
$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
*/
@@ -41,6 +130,68 @@ class GenericContainerTest extends TestCase
$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'))
@@ -66,6 +217,26 @@ class GenericContainerTest extends TestCase
$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'))
@@ -95,6 +266,8 @@ class GenericContainerTest extends TestCase
$result = $container->exec(["cat", $containerPath.'/test.txt']);
self::assertSame('hello world', $result);
$container->stop();
}
public function testShouldSetPrivilegedMode(): void
@@ -109,5 +282,7 @@ class GenericContainerTest extends TestCase
$privileged = $inspectResult?->getHostConfig()?->getPrivileged();
self::assertTrue($privileged);
$container->stop();
}
}
+218
View File
@@ -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);
}
}