Description
Symfony version(s) affected
5.4, 6.1
Description
I believe this issue was introduced by #40144, which made a change to doRemove
to make copies of directories with temporary names before removing them.
I came across this while attempting to update dependencies in a legacy library which is a dependency of our main application.
When I run the tests for this legacy library, I get this:
> vendor/bin/phpunit
PHPUnit 9.5.25 #StandWithUkraine
Runtime: PHP 8.1.11
Configuration: /var/other/flex/phpunit.xml
.....E...... 12 / 12 (100%)
Time: 00:00.095, Memory: 4.00 MB
There was 1 error:
1) IconLanguageServices\Flex\Test\Integration\FolderArchiverTest::test_it_can_move_files_to_archive_directory
IconLanguageServices\Flex\Exception\CouldNotArchive: Failed to archiveFailed to remove directory "/var/other/flex/test/Integration/TestFileSystem/src/..YzH": rmdir(/var/other/flex/test/Integration/TestFileSystem/src/..YzH): Text file busy
/var/other/flex/src/FolderArchiver.php:58
/var/other/flex/test/Integration/FolderArchiverTest.php:60
Caused by
Symfony\Component\Filesystem\Exception\IOException: Failed to remove directory "/var/other/flex/test/Integration/TestFileSystem/src/..YzH": rmdir(/var/other/flex/test/Integration/TestFileSystem/src/..YzH): Text file busy
/var/other/flex/vendor/symfony/filesystem/Filesystem.php:191
/var/other/flex/vendor/symfony/filesystem/Filesystem.php:150
/var/other/flex/src/FolderArchiver.php:56
/var/other/flex/test/Integration/FolderArchiverTest.php:60
ERRORS!
Tests: 12, Assertions: 15, Errors: 1.
The test which fails looks like this:
<?php
namespace IconLanguageServices\Flex\Test\Integration;
use IconLanguageServices\Flex\FolderArchiver;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
class FolderArchiverTest extends \IconLanguageServices\Flex\Test\TestCase
{
private Filesystem $fs;
private string $src;
private string $tgt;
private FolderArchiver $archiver;
public function setUp(): void
{
parent::setUp();
$this->fs = new Filesystem();
$this->src = __DIR__ . "/TestFileSystem/src/123";
$this->tgt = __DIR__ . "/TestFileSystem/tgt";
if (!$this->fs->exists($this->src)) {
$this->fs->mkdir($this->src);
}
if (!$this->fs->exists("{$this->src}/file1")) {
$this->fs->touch("{$this->src}/file1");
}
if (!$this->fs->exists("{$this->src}/file2")) {
$this->fs->touch("{$this->src}/file2");
}
if (!$this->fs->exists($this->tgt)) {
$this->fs->mkdir($this->tgt);
}
$this->archiver = new FolderArchiver($this->fs, new Finder(), $this->tgt);
}
public function tearDown(): void
{
parent::tearDown();
if ($this->fs->exists($this->src)) {
$this->fs->remove($this->src);
}
if ($this->fs->exists($this->tgt)) {
$this->fs->remove($this->tgt);
}
}
/**
* @return void
*/
public function test_it_can_move_files_to_archive_directory()
{
$this->archiver->archive($this->src);
$this->assertFalse(
file_exists($this->src),
"Source folder has not been moved"
);
$date = date("Y-m-d");
$this->assertTrue(
file_exists($this->tgt . DIRECTORY_SEPARATOR . $date . DIRECTORY_SEPARATOR . "123"),
"Folder named '123' not found in archive folder"
);
$this->assertTrue(
file_exists($this->tgt . DIRECTORY_SEPARATOR . $date . DIRECTORY_SEPARATOR . "123" . DIRECTORY_SEPARATOR . "file1"),
"File named 'file1' not found in archive sub-folder"
);
$this->assertTrue(
file_exists($this->tgt . DIRECTORY_SEPARATOR . $date . DIRECTORY_SEPARATOR . "123" . DIRECTORY_SEPARATOR . "file2"),
"File named 'file2' not found in archive sub-folder"
);
}
}
The subject-under-test looks like this:
<?php
namespace IconLanguageServices\Flex;
use IconLanguageServices\Flex\Exception\CouldNotArchive;
use IconLanguageServices\Flex\Exception\FolderNotFound;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
class FolderArchiver
{
/**
* @var Filesystem
*/
private $filesystem;
/**
* @var string
*/
private $archiveFolder;
/**
* @var Finder
*/
private $finder;
public function __construct(Filesystem $filesystem, Finder $finder, $archiveFolder)
{
$this->filesystem = $filesystem;
$this->finder = $finder;
if (!$filesystem->exists($archiveFolder)) {
$filesystem->mkdir($archiveFolder);
}
$this->archiveFolder = $archiveFolder;
}
/**
* @param string $folder
* @return void
* @throws FolderNotFound
*/
public function archive($folder)
{
if (!$this->filesystem->exists($folder)) {
throw new FolderNotFound($folder . " not found");
}
try {
foreach ($this->finder->files()->in($folder) as $file) {
/** @var $file SplFileInfo */
$this->filesystem->copy($file, $this->getArchiveDestination($folder) . $file->getFilename());
}
$this->filesystem->remove($folder);
} catch (IOException $e) {
throw new CouldNotArchive("Failed to archive" . $e->getMessage(), $e->getCode(), $e);
}
}
/**
* @param $folder
* @return string
*/
private function getArchiveDestination($folder)
{
$parts = array_filter(preg_split("/[\/\\\\]/", $folder));
return $this->archiveFolder . DIRECTORY_SEPARATOR . date("Y-m-d") . DIRECTORY_SEPARATOR . array_pop($parts) . DIRECTORY_SEPARATOR;
}
}
I am running PHP 8.1.1 inside a RHEL VM using Vagrant 2.3.0 and VirtualBox, with Windows 10 as the host OS. All files in the library's dir are shared from a directory in the host OS.
All tests passed in this same environment before I changed the dependency requirements.
I installed PHP 8.1 for Windows in the host OS and ran the tests in cmd.exe, and they all passed. The tests all pass in our CI environment (Jenkins on RHEL) as well. I am therefore confident that this is related to Vagrant.
How to reproduce
Run the PHPUnit test described above, for the SUT described above, on PHP 8.1 inside a Vagrant VM, with the files shared from a host OS.
Possible Solution
No response
Additional Context
No response