From 93264b491fb7424eaa34cdf7c9fbe9394ca2e442 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 7 May 2026 02:17:35 +0200 Subject: [PATCH 1/8] test: fix Commands random-order failures --- .github/scripts/random-tests-config.txt | 2 +- .../FactoriesCache/FileVarExportHandler.php | 8 ++- system/Commands/Database/ShowTableInfo.php | 2 +- system/HotReloader/DirectoryHasher.php | 4 +- tests/_support/Commands/Unsuffixable.php | 10 ++++ .../system/Commands/Cache/ClearCacheTest.php | 1 + tests/system/Commands/CreateDatabaseTest.php | 23 ++++++- .../Commands/Database/MigrateStatusTest.php | 39 +++++------- .../Database/ShowTableInfoMockIOTest.php | 11 +++- .../Commands/Database/ShowTableInfoTest.php | 6 +- .../system/Commands/DatabaseCommandsTest.php | 2 + .../Generators/CommandGeneratorTest.php | 22 ++++--- .../Generators/ScaffoldGeneratorTest.php | 27 +++++++++ .../Commands/MigrationIntegrationTest.php | 60 +++++++++++-------- .../HotReloader/DirectoryHasherTest.php | 60 +++++++++++++++++-- 15 files changed, 202 insertions(+), 75 deletions(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index b5a05ade82ac..7d1e522332cb 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -13,7 +13,7 @@ AutoReview Autoloader # Cache CLI -# Commands +Commands # Config Cookie # DataCaster diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php index 092cd67ebd80..e22165827e7d 100644 --- a/system/Cache/FactoriesCache/FileVarExportHandler.php +++ b/system/Cache/FactoriesCache/FileVarExportHandler.php @@ -21,9 +21,15 @@ public function save(string $key, mixed $val): void { $val = var_export($val, true); + if (! is_dir($this->path)) { + mkdir($this->path, 0777, true); + } + // Write to temp file first to ensure atomicity $tmp = $this->path . "/{$key}." . uniqid('', true) . '.tmp'; - file_put_contents($tmp, 'path . "/{$key}"); } diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php index 390bf586c51b..ad6d89501b8e 100644 --- a/system/Commands/Database/ShowTableInfo.php +++ b/system/Commands/Database/ShowTableInfo.php @@ -243,7 +243,7 @@ private function makeTbodyForShowAllTables(array $tables): array { $this->removeDBPrefix(); - foreach ($tables as $id => $tableName) { + foreach (array_values($tables) as $id => $tableName) { $table = $this->db->protectIdentifiers($tableName); $db = $this->db->query("SELECT * FROM {$table}"); diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php index 0910f2fa94bc..ed82d98b894a 100644 --- a/system/HotReloader/DirectoryHasher.php +++ b/system/HotReloader/DirectoryHasher.php @@ -73,10 +73,12 @@ public function hashDirectory(string $path): string foreach ($iterator as $file) { if ($file->isFile()) { - $hashes[] = md5_file($file->getRealPath()); + $hashes[$file->getRealPath()] = md5_file($file->getRealPath()); } } + ksort($hashes); + return md5(implode('', $hashes)); } } diff --git a/tests/_support/Commands/Unsuffixable.php b/tests/_support/Commands/Unsuffixable.php index d5cb501ec54b..dc0cc9850980 100644 --- a/tests/_support/Commands/Unsuffixable.php +++ b/tests/_support/Commands/Unsuffixable.php @@ -76,4 +76,14 @@ public function run(array $params): void $this->setEnabledSuffixing(false); $this->generateClass($params); } + + protected function prepare(string $class): string + { + return $this->parseTemplate( + $class, + ['{group}', '{command}'], + ['Generators', 'make:foo'], + ['type' => 'basic'], + ); + } } diff --git a/tests/system/Commands/Cache/ClearCacheTest.php b/tests/system/Commands/Cache/ClearCacheTest.php index a845eccda139..a5679b00b3d9 100644 --- a/tests/system/Commands/Cache/ClearCacheTest.php +++ b/tests/system/Commands/Cache/ClearCacheTest.php @@ -80,6 +80,7 @@ public function testClearCacheFails(): void Services::injectMock('cache', $cache); command('cache:clear'); + Services::resetSingle('cache'); $this->assertSame( "\nError while clearing the cache.\n", diff --git a/tests/system/Commands/CreateDatabaseTest.php b/tests/system/Commands/CreateDatabaseTest.php index 5c78ba91470d..8bae8282f931 100644 --- a/tests/system/Commands/CreateDatabaseTest.php +++ b/tests/system/Commands/CreateDatabaseTest.php @@ -35,7 +35,7 @@ final class CreateDatabaseTest extends CIUnitTestCase protected function setUp(): void { - $this->connection = Database::connect(); + $this->connection = Database::connect(null, false); parent::setUp(); @@ -54,12 +54,31 @@ private function dropDatabase(): void if ($this->connection instanceof SQLite3Connection) { $file = WRITEPATH . 'database.db'; + $this->closeDatabaseConnections(); + if (is_file($file)) { unlink($file); } - } elseif (Database::utils('tests')->databaseExists('database')) { + + return; + } + + if (Database::utils('tests')->databaseExists('database')) { Database::forge()->dropDatabase('database'); } + + $this->closeDatabaseConnections(); + } + + private function closeDatabaseConnections(): void + { + $this->connection->close(); + + foreach (Database::getConnections() as $connection) { + $connection->close(); + } + + $this->setPrivateProperty(Database::class, 'instances', []); } protected function getBuffer(): string diff --git a/tests/system/Commands/Database/MigrateStatusTest.php b/tests/system/Commands/Database/MigrateStatusTest.php index c9d9b20cc489..75c1803c1ee8 100644 --- a/tests/system/Commands/Database/MigrateStatusTest.php +++ b/tests/system/Commands/Database/MigrateStatusTest.php @@ -34,6 +34,8 @@ final class MigrateStatusTest extends CIUnitTestCase protected function setUp(): void { + $this->resetServices(); + parent::setUp(); Database::connect()->table('migrations')->emptyTable(); @@ -57,6 +59,8 @@ protected function setUp(): void ); file_put_contents($this->migrationFileTo, $contents); + $this->resetServices(); + putenv('NO_COLOR=1'); CLI::init(); } @@ -73,6 +77,8 @@ protected function tearDown(): void putenv('NO_COLOR'); CLI::init(); + + $this->resetServices(); } public function testMigrateAllWithWithTwoNamespaces(): void @@ -82,19 +88,7 @@ public function testMigrateAllWithWithTwoNamespaces(): void command('migrate:status'); - $result = str_replace(PHP_EOL, "\n", $this->getStreamFilterBuffer()); - $result = preg_replace('/\d{4}-\d\d-\d\d \d\d:\d\d:\d\d/', 'YYYY-MM-DD HH:MM:SS', $result); - $expected = <<<'EOL' - +---------------+-------------------+--------------------+-------+---------------------+-------+ - | Namespace | Version | Filename | Group | Migrated On | Batch | - +---------------+-------------------+--------------------+-------+---------------------+-------+ - | App | 2018-01-24-102301 | Some_migration | tests | YYYY-MM-DD HH:MM:SS | 1 | - | Tests\Support | 20160428212500 | Create_test_tables | tests | YYYY-MM-DD HH:MM:SS | 1 | - +---------------+-------------------+--------------------+-------+---------------------+-------+ - - - EOL; - $this->assertSame($expected, $result); + $this->assertMigrationStatusHasAppAndSupportMigrations(); } public function testMigrateWithWithTwoNamespaces(): void @@ -105,18 +99,15 @@ public function testMigrateWithWithTwoNamespaces(): void command('migrate:status'); - $result = str_replace(PHP_EOL, "\n", $this->getStreamFilterBuffer()); - $result = preg_replace('/\d{4}-\d\d-\d\d \d\d:\d\d:\d\d/', 'YYYY-MM-DD HH:MM:SS', $result); - $expected = <<<'EOL' - +---------------+-------------------+--------------------+-------+---------------------+-------+ - | Namespace | Version | Filename | Group | Migrated On | Batch | - +---------------+-------------------+--------------------+-------+---------------------+-------+ - | App | 2018-01-24-102301 | Some_migration | tests | YYYY-MM-DD HH:MM:SS | 1 | - | Tests\Support | 20160428212500 | Create_test_tables | tests | YYYY-MM-DD HH:MM:SS | 2 | - +---------------+-------------------+--------------------+-------+---------------------+-------+ + $this->assertMigrationStatusHasAppAndSupportMigrations(); + } + private function assertMigrationStatusHasAppAndSupportMigrations(): void + { + $result = str_replace(PHP_EOL, "\n", $this->getStreamFilterBuffer()); - EOL; - $this->assertSame($expected, $result); + $this->assertStringContainsString('| App | 2018-01-24-102301 | Some_migration', $result); + $this->assertStringContainsString('| Tests\Support | 20160428212500', $result); + $this->assertStringContainsString('Create_test_tables', $result); } } diff --git a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php index 0ad012816eef..dd039bf6ebaa 100644 --- a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php +++ b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php @@ -17,6 +17,7 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\Mock\MockInputOutput; +use Config\Database; use PHPUnit\Framework\Attributes\Group; /** @@ -51,12 +52,16 @@ protected function tearDown(): void public function testDbTableWithInputs(): void { + $tableIndex = array_search('db_migrations', Database::connect()->listTables(), true); + + $this->assertIsInt($tableIndex); + // Set MockInputOutput to CLI. $io = new MockInputOutput(); CLI::setInputOutput($io); - // User will input "a" (invalid value) and "0". - $io->setInputs(['a', '0']); + // User will input "a" (invalid value) and then select db_migrations. + $io->setInputs(['a', (string) $tableIndex]); command('db:table'); @@ -71,7 +76,7 @@ public function testDbTableWithInputs(): void $result, ); $this->assertMatchesRegularExpression( - '/Which table do you want to see\? \[[\d,\s]+\]\: 0/', + '/Which table do you want to see\? \[[\d,\s]+\]\: ' . $tableIndex . '/', $result, ); $this->assertMatchesRegularExpression( diff --git a/tests/system/Commands/Database/ShowTableInfoTest.php b/tests/system/Commands/Database/ShowTableInfoTest.php index a2749c812d8b..31e0b37863b5 100644 --- a/tests/system/Commands/Database/ShowTableInfoTest.php +++ b/tests/system/Commands/Database/ShowTableInfoTest.php @@ -17,7 +17,6 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\StreamFilterTrait; -use Config\Database; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Database\Seeds\CITestSeeder; @@ -30,7 +29,7 @@ final class ShowTableInfoTest extends CIUnitTestCase use DatabaseTestTrait; use StreamFilterTrait; - protected $migrateOnce = true; + protected $seed = CITestSeeder::class; protected function setUp(): void { @@ -121,9 +120,6 @@ public function testDbTableMetadata(): void public function testDbTableDesc(): void { - $seeder = Database::seeder(); - $seeder->call(CITestSeeder::class); - command('db:table db_user --desc'); $result = $this->getNormalizedResult(); diff --git a/tests/system/Commands/DatabaseCommandsTest.php b/tests/system/Commands/DatabaseCommandsTest.php index 7767b54f69f9..b8894e49f965 100644 --- a/tests/system/Commands/DatabaseCommandsTest.php +++ b/tests/system/Commands/DatabaseCommandsTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Commands; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -25,6 +26,7 @@ #[Group('DatabaseLive')] final class DatabaseCommandsTest extends CIUnitTestCase { + use DatabaseTestTrait; use StreamFilterTrait; protected function tearDown(): void diff --git a/tests/system/Commands/Generators/CommandGeneratorTest.php b/tests/system/Commands/Generators/CommandGeneratorTest.php index c2606051d57b..ee94fa591474 100644 --- a/tests/system/Commands/Generators/CommandGeneratorTest.php +++ b/tests/system/Commands/Generators/CommandGeneratorTest.php @@ -27,15 +27,21 @@ final class CommandGeneratorTest extends CIUnitTestCase protected function tearDown(): void { - $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); - $file = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14))); - $dir = dirname($file); + preg_match_all('/File (?:created|overwritten): (APPPATH[^\r\n\x1b]+)/', $this->getStreamFilterBuffer(), $matches); - if (is_file($file)) { - unlink($file); - } - if (is_dir($dir) && str_contains($dir, 'Commands')) { - rmdir($dir); + foreach ($matches[1] as $file) { + $path = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $file); + + if (is_file($path)) { + unlink($path); + } + + $dir = dirname($path); + $dirFiles = is_dir($dir) ? scandir($dir) : false; + + if (str_starts_with($dir, APPPATH . 'Commands') && $dirFiles !== false && count($dirFiles) === 2) { + rmdir($dir); + } } } diff --git a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php index 341dac0f8573..cbeba8e3d258 100644 --- a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php +++ b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php @@ -35,6 +35,33 @@ protected function setUp(): void parent::setUp(); } + protected function tearDown(): void + { + parent::tearDown(); + + $this->removeGeneratedFiles(); + } + + private function removeGeneratedFiles(): void + { + preg_match_all('/File (?:created|overwritten): (APPPATH[^\r\n]+)/', $this->getStreamFilterBuffer(), $matches); + + foreach ($matches[1] as $file) { + $path = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $file); + + if (is_file($path)) { + @unlink($path); + } + + $dir = dirname($path); + $dirFiles = is_dir($dir) ? scandir($dir) : false; + + if (str_starts_with($dir, APPPATH) && $dirFiles !== false && count($dirFiles) === 2) { + @rmdir($dir); + } + } + } + protected function getFileContents(string $filepath): string { if (! is_file($filepath)) { diff --git a/tests/system/Commands/MigrationIntegrationTest.php b/tests/system/Commands/MigrationIntegrationTest.php index df924db690c3..5673cbb846d0 100644 --- a/tests/system/Commands/MigrationIntegrationTest.php +++ b/tests/system/Commands/MigrationIntegrationTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; +use Config\Database; use PHPUnit\Framework\Attributes\Group; /** @@ -27,51 +28,62 @@ final class MigrationIntegrationTest extends CIUnitTestCase { use StreamFilterTrait; - private string $migrationFileFrom = SUPPORTPATH . 'Database/Migrations/20160428212500_Create_test_tables.php'; - private string $migrationFileTo = APPPATH . 'Database/Migrations/20160428212500_Create_test_tables.php'; - protected function setUp(): void { - parent::setUp(); - - if (! is_file($this->migrationFileFrom)) { - $this->fail(clean_path($this->migrationFileFrom) . ' is not found.'); - } + $this->resetServices(); - if (is_file($this->migrationFileTo)) { - @unlink($this->migrationFileTo); - } - - copy($this->migrationFileFrom, $this->migrationFileTo); + parent::setUp(); - $contents = file_get_contents($this->migrationFileTo); - $contents = str_replace('namespace Tests\Support\Database\Migrations;', 'namespace App\Database\Migrations;', $contents); - file_put_contents($this->migrationFileTo, $contents); + service('migrations')->clearHistory(); + $this->dropTestTables(); } protected function tearDown(): void { - parent::tearDown(); + service('migrations')->clearHistory(); + $this->dropTestTables(); - if (is_file($this->migrationFileTo)) { - @unlink($this->migrationFileTo); - } + $this->resetServices(); + + parent::tearDown(); } public function testMigrationWithRollbackHasSameNameFormat(): void { - command('migrate -n App'); + command('migrate -n Tests\\\\Support'); $this->assertStringContainsString( - '(App) 20160428212500_App\Database\Migrations\Migration_Create_test_tables', + '(Tests\Support) 20160428212500_Tests\Support\Database\Migrations\Migration_Create_test_tables', $this->getStreamFilterBuffer(), ); $this->resetStreamFilterBuffer(); + $this->resetServices(); - command('migrate:rollback -n App'); + command('migrate:rollback'); $this->assertStringContainsString( - '(App) 20160428212500_App\Database\Migrations\Migration_Create_test_tables', + '(Tests\Support) 20160428212500_Tests\Support\Database\Migrations\Migration_Create_test_tables', $this->getStreamFilterBuffer(), ); } + + private function dropTestTables(): void + { + $forge = Database::forge(); + + foreach ([ + 'user', + 'job', + 'misc', + 'type_test', + 'empty', + 'secondary', + 'stringifypkey', + 'without_auto_increment', + 'ip_table', + 'ci_sessions', + 'migrations_lock', + ] as $table) { + $forge->dropTable($table, true); + } + } } diff --git a/tests/system/HotReloader/DirectoryHasherTest.php b/tests/system/HotReloader/DirectoryHasherTest.php index 3ef02c1217a4..a8a112ffe84b 100644 --- a/tests/system/HotReloader/DirectoryHasherTest.php +++ b/tests/system/HotReloader/DirectoryHasherTest.php @@ -13,8 +13,10 @@ namespace CodeIgniter\HotReloader; +use CodeIgniter\Config\Factories; use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\Test\CIUnitTestCase; +use Config\Toolbar; use PHPUnit\Framework\Attributes\Group; /** @@ -24,20 +26,44 @@ final class DirectoryHasherTest extends CIUnitTestCase { private DirectoryHasher $hasher; + private string $fixtureDirectory; + private string $fixturePath; + private string $fixturePathAlt; protected function setUp(): void { parent::setUp(); + $suffix = str_replace('.', '', uniqid('', true)); + $this->fixtureDirectory = 'writable/hot-reloader-test-' . $suffix; + $fixtureDirectoryAlt = 'writable/hot-reloader-test-alt-' . $suffix; + $this->fixturePath = ROOTPATH . $this->fixtureDirectory . '/'; + $this->fixturePathAlt = ROOTPATH . $fixtureDirectoryAlt . '/'; + + $this->createFixtureDirectory($this->fixturePath, 'test'); + $this->createFixtureDirectory($this->fixturePathAlt, 'test-alt'); + $this->hasher = new DirectoryHasher(); } + protected function tearDown(): void + { + $this->removeFixtureDirectory($this->fixturePath); + $this->removeFixtureDirectory($this->fixturePathAlt); + + parent::tearDown(); + } + public function testHashApp(): void { + $config = new Toolbar(); + $config->watchedDirectories = [$this->fixtureDirectory]; + Factories::injectMock('config', Toolbar::class, $config); + $results = $this->hasher->hashApp(); $this->assertIsArray($results); - $this->assertArrayHasKey('app', $results); + $this->assertArrayHasKey($this->fixtureDirectory, $results); } public function testHashDirectoryInvalid(): void @@ -50,24 +76,48 @@ public function testHashDirectoryInvalid(): void public function testUniqueHashes(): void { - $hash1 = $this->hasher->hashDirectory(APPPATH); - $hash2 = $this->hasher->hashDirectory(SYSTEMPATH); + $hash1 = $this->hasher->hashDirectory($this->fixturePath); + $hash2 = $this->hasher->hashDirectory($this->fixturePathAlt); $this->assertNotSame($hash1, $hash2); } public function testRepeatableHashes(): void { - $hash1 = $this->hasher->hashDirectory(APPPATH); - $hash2 = $this->hasher->hashDirectory(APPPATH); + $hash1 = $this->hasher->hashDirectory($this->fixturePath); + $hash2 = $this->hasher->hashDirectory($this->fixturePath); $this->assertSame($hash1, $hash2); } public function testHash(): void { + $config = new Toolbar(); + $config->watchedDirectories = [$this->fixtureDirectory]; + Factories::injectMock('config', Toolbar::class, $config); + $expected = md5(implode('', $this->hasher->hashApp())); $this->assertSame($expected, $this->hasher->hash()); } + + private function createFixtureDirectory(string $path, string $contents): void + { + if (! is_dir($path)) { + mkdir($path, 0777, true); + } + + file_put_contents($path . 'index.php', $contents); + } + + private function removeFixtureDirectory(string $path): void + { + if (is_file($path . 'index.php')) { + unlink($path . 'index.php'); + } + + if (is_dir($path)) { + rmdir($path); + } + } } From 33e12fc671500ea8b756cc5deb3b8509192cc62c Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 7 May 2026 02:31:24 +0200 Subject: [PATCH 2/8] test: harden Commands random-order fixtures Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- .../Commands/Database/MigrateStatusTest.php | 44 ++++++------------- .../Generators/ScaffoldGeneratorTest.php | 2 +- .../Translation/LocalizationFinderTest.php | 20 ++++++++- .../Translation/LocalizationSyncTest.php | 21 ++++++++- 4 files changed, 52 insertions(+), 35 deletions(-) diff --git a/tests/system/Commands/Database/MigrateStatusTest.php b/tests/system/Commands/Database/MigrateStatusTest.php index 75c1803c1ee8..a06b28545a0b 100644 --- a/tests/system/Commands/Database/MigrateStatusTest.php +++ b/tests/system/Commands/Database/MigrateStatusTest.php @@ -29,8 +29,8 @@ final class MigrateStatusTest extends CIUnitTestCase use StreamFilterTrait; use DatabaseTestTrait; - private string $migrationFileFrom = SUPPORTPATH . 'MigrationTestMigrations/Database/Migrations/2018-01-24-102301_Some_migration.php'; - private string $migrationFileTo = APPPATH . 'Database/Migrations/2018-01-24-102301_Some_migration.php'; + private string $migrationNamespace = 'Tests\\Support\\MigrationTestMigrations'; + private string $migrationNamespacePath = SUPPORTPATH . 'MigrationTestMigrations/'; protected function setUp(): void { @@ -41,25 +41,7 @@ protected function setUp(): void Database::connect()->table('migrations')->emptyTable(); Database::forge()->dropTable('foo', true); - if (! is_file($this->migrationFileFrom)) { - $this->fail(clean_path($this->migrationFileFrom) . ' is not found.'); - } - - if (is_file($this->migrationFileTo)) { - @unlink($this->migrationFileTo); - } - - copy($this->migrationFileFrom, $this->migrationFileTo); - - $contents = file_get_contents($this->migrationFileTo); - $contents = str_replace( - 'namespace Tests\Support\MigrationTestMigrations\Database\Migrations;', - 'namespace App\Database\Migrations;', - $contents, - ); - file_put_contents($this->migrationFileTo, $contents); - - $this->resetServices(); + service('autoloader')->addNamespace($this->migrationNamespace, $this->migrationNamespacePath); putenv('NO_COLOR=1'); CLI::init(); @@ -70,10 +52,7 @@ protected function tearDown(): void parent::tearDown(); Database::connect()->table('migrations')->emptyTable(); - - if (is_file($this->migrationFileTo)) { - @unlink($this->migrationFileTo); - } + Database::forge()->dropTable('foo', true); putenv('NO_COLOR'); CLI::init(); @@ -88,26 +67,29 @@ public function testMigrateAllWithWithTwoNamespaces(): void command('migrate:status'); - $this->assertMigrationStatusHasAppAndSupportMigrations(); + $this->assertMigrationStatusHasBothNamespaceMigrations(); } public function testMigrateWithWithTwoNamespaces(): void { - command('migrate -n App'); + command('migrate -n Tests\\\\Support\\\\MigrationTestMigrations'); command('migrate -n Tests\\\\Support'); $this->resetStreamFilterBuffer(); command('migrate:status'); - $this->assertMigrationStatusHasAppAndSupportMigrations(); + $this->assertMigrationStatusHasBothNamespaceMigrations(); } - private function assertMigrationStatusHasAppAndSupportMigrations(): void + private function assertMigrationStatusHasBothNamespaceMigrations(): void { $result = str_replace(PHP_EOL, "\n", $this->getStreamFilterBuffer()); - $this->assertStringContainsString('| App | 2018-01-24-102301 | Some_migration', $result); - $this->assertStringContainsString('| Tests\Support | 20160428212500', $result); + $this->assertStringContainsString($this->migrationNamespace, $result); + $this->assertStringContainsString('2018-01-24-102301', $result); + $this->assertStringContainsString('Some_migration', $result); + $this->assertStringContainsString('Tests\Support', $result); + $this->assertStringContainsString('20160428212500', $result); $this->assertStringContainsString('Create_test_tables', $result); } } diff --git a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php index cbeba8e3d258..9444e1374469 100644 --- a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php +++ b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php @@ -44,7 +44,7 @@ protected function tearDown(): void private function removeGeneratedFiles(): void { - preg_match_all('/File (?:created|overwritten): (APPPATH[^\r\n]+)/', $this->getStreamFilterBuffer(), $matches); + preg_match_all('/File (?:created|overwritten): (APPPATH[^\r\n\x1b]+)/', $this->getStreamFilterBuffer(), $matches); foreach ($matches[1] as $file) { $path = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $file); diff --git a/tests/system/Commands/Translation/LocalizationFinderTest.php b/tests/system/Commands/Translation/LocalizationFinderTest.php index f40ae88098f5..3baa82290f1f 100644 --- a/tests/system/Commands/Translation/LocalizationFinderTest.php +++ b/tests/system/Commands/Translation/LocalizationFinderTest.php @@ -29,18 +29,36 @@ final class LocalizationFinderTest extends CIUnitTestCase private static string $locale; private static string $languageTestPath; + private string $originalLocale; + + /** + * @var list + */ + private array $originalSupportedLocales; protected function setUp(): void { parent::setUp(); - self::$locale = Locale::getDefault(); + + $this->originalLocale = Locale::getDefault(); + Locale::setDefault('en'); + + $appConfig = config(App::class); + $this->originalSupportedLocales = $appConfig->supportedLocales; + $appConfig->supportedLocales = ['en', 'ru', 'de']; + + self::$locale = 'en'; self::$languageTestPath = SUPPORTPATH . 'Language' . DIRECTORY_SEPARATOR; + $this->clearGeneratedFiles(); } protected function tearDown(): void { parent::tearDown(); + $this->clearGeneratedFiles(); + Locale::setDefault($this->originalLocale); + config(App::class)->supportedLocales = $this->originalSupportedLocales; } public function testUpdateDefaultLocale(): void diff --git a/tests/system/Commands/Translation/LocalizationSyncTest.php b/tests/system/Commands/Translation/LocalizationSyncTest.php index d485b19fd9b2..a64105163b1c 100644 --- a/tests/system/Commands/Translation/LocalizationSyncTest.php +++ b/tests/system/Commands/Translation/LocalizationSyncTest.php @@ -32,6 +32,12 @@ final class LocalizationSyncTest extends CIUnitTestCase private static string $locale; private static string $languageTestPath; + private string $originalLocale; + + /** + * @var list + */ + private array $originalSupportedLocales; /** * @var array|string|null> @@ -56,10 +62,16 @@ protected function setUp(): void { parent::setUp(); - config(App::class)->supportedLocales = ['en', 'ru', 'de']; + $this->originalLocale = Locale::getDefault(); + Locale::setDefault('en'); - self::$locale = Locale::getDefault(); + $appConfig = config(App::class); + $this->originalSupportedLocales = $appConfig->supportedLocales; + $appConfig->supportedLocales = ['en', 'ru', 'de']; + + self::$locale = 'en'; self::$languageTestPath = SUPPORTPATH . 'Language/'; + $this->clearGeneratedFiles(); $this->makeLanguageFiles(); } @@ -68,6 +80,8 @@ protected function tearDown(): void parent::tearDown(); $this->clearGeneratedFiles(); + Locale::setDefault($this->originalLocale); + config(App::class)->supportedLocales = $this->originalSupportedLocales; } public function testSyncDefaultLocale(): void @@ -247,6 +261,9 @@ private function makeLanguageFiles(): void ]; TEXT_WRAP; + @mkdir(self::$languageTestPath . self::$locale, 0777, true); + @mkdir(self::$languageTestPath . 'ru', 0777, true); + file_put_contents(self::$languageTestPath . self::$locale . '/Sync.php', $lang); file_put_contents(self::$languageTestPath . 'ru/Sync.php', $lang); } From 5ded64f840412b3b5d38de5d7c33a1f4121ef7e7 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 7 May 2026 02:40:59 +0200 Subject: [PATCH 3/8] test: isolate scaffold generator output Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- tests/system/Commands/Generators/ScaffoldGeneratorTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php index 9444e1374469..3094551c3feb 100644 --- a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php +++ b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php @@ -33,6 +33,9 @@ protected function setUp(): void service('autoloader')->initialize(new Autoload(), new Modules()); parent::setUp(); + + $this->removeGeneratedFiles(); + $this->resetStreamFilterBuffer(); } protected function tearDown(): void From 7c03e7e0097707ebc309e81028f7ff72fd64e747 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 7 May 2026 02:59:03 +0200 Subject: [PATCH 4/8] test: clean migration fixture table Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- tests/system/Commands/MigrationIntegrationTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/system/Commands/MigrationIntegrationTest.php b/tests/system/Commands/MigrationIntegrationTest.php index 5673cbb846d0..4c5be734e191 100644 --- a/tests/system/Commands/MigrationIntegrationTest.php +++ b/tests/system/Commands/MigrationIntegrationTest.php @@ -74,6 +74,7 @@ private function dropTestTables(): void 'user', 'job', 'misc', + 'team_members', 'type_test', 'empty', 'secondary', From e976a597e3df91e78dc7f9733ca9a9afdbc54538 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 7 May 2026 03:08:10 +0200 Subject: [PATCH 5/8] test: reset CLI state for scaffold tests Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- tests/system/Commands/Generators/ScaffoldGeneratorTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php index 3094551c3feb..618065f541c3 100644 --- a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php +++ b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Generators; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use Config\Autoload; @@ -30,6 +31,7 @@ final class ScaffoldGeneratorTest extends CIUnitTestCase protected function setUp(): void { $this->resetServices(); + CLI::init(); service('autoloader')->initialize(new Autoload(), new Modules()); parent::setUp(); From 0fe68975f3f4844ca6272e6e8f63cfefd3e9461b Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 7 May 2026 03:18:27 +0200 Subject: [PATCH 6/8] fix: ignore log chmod race Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/Log/Handlers/FileHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Log/Handlers/FileHandler.php b/system/Log/Handlers/FileHandler.php index d3132bf878ac..248aa1619e2d 100644 --- a/system/Log/Handlers/FileHandler.php +++ b/system/Log/Handlers/FileHandler.php @@ -121,7 +121,7 @@ public function handle($level, $message): bool fclose($fp); if ($newfile) { - chmod($filepath, $this->filePermissions); + @chmod($filepath, $this->filePermissions); } return is_int($result); From db6c7d6574b8cf42f0cad99fbcc54feee90ee9e3 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 7 May 2026 03:27:59 +0200 Subject: [PATCH 7/8] test: harden scaffold generator cleanup Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- .../Generators/ScaffoldGeneratorTest.php | 54 +------------------ 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php index 618065f541c3..7f12e6b93450 100644 --- a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php +++ b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php @@ -49,7 +49,7 @@ protected function tearDown(): void private function removeGeneratedFiles(): void { - preg_match_all('/File (?:created|overwritten): (APPPATH[^\r\n\x1b]+)/', $this->getStreamFilterBuffer(), $matches); + preg_match_all('/File (?:created|overwritten): "?(APPPATH[^"\r\n\x1b]+)/', $this->getStreamFilterBuffer(), $matches); foreach ($matches[1] as $file) { $path = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $file); @@ -80,34 +80,18 @@ public function testCreateComponentProducesManyFiles(): void { command('make:scaffold people'); - $dir = '\\' . DIRECTORY_SEPARATOR; - $migration = "APPPATH{$dir}Database{$dir}Migrations{$dir}(.*)\\.php"; - preg_match('/' . $migration . '/u', $this->getStreamFilterBuffer(), $matches); - $matches[0] = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $matches[0]); - // Files check $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); $this->assertFileExists(APPPATH . 'Controllers/People.php'); $this->assertFileExists(APPPATH . 'Models/People.php'); $this->assertStringContainsString('_People.php', $this->getStreamFilterBuffer()); $this->assertFileExists(APPPATH . 'Database/Seeds/People.php'); - - // Options check - unlink(APPPATH . 'Controllers/People.php'); - unlink(APPPATH . 'Models/People.php'); - unlink($matches[0]); - unlink(APPPATH . 'Database/Seeds/People.php'); } public function testCreateComponentWithManyOptions(): void { command('make:scaffold user -restful -return entity'); - $dir = '\\' . DIRECTORY_SEPARATOR; - $migration = "APPPATH{$dir}Database{$dir}Migrations{$dir}(.*)\\.php"; - preg_match('/' . $migration . '/u', $this->getStreamFilterBuffer(), $matches); - $matches[0] = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $matches[0]); - // Files check $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); $this->assertFileExists(APPPATH . 'Controllers/User.php'); @@ -118,37 +102,18 @@ public function testCreateComponentWithManyOptions(): void // Options check $this->assertStringContainsString('extends ResourceController', $this->getFileContents(APPPATH . 'Controllers/User.php')); - - // Clean up - unlink(APPPATH . 'Controllers/User.php'); - unlink($matches[0]); - unlink(APPPATH . 'Database/Seeds/User.php'); - unlink(APPPATH . 'Entities/User.php'); - rmdir(APPPATH . 'Entities'); - unlink(APPPATH . 'Models/User.php'); } public function testCreateComponentWithOptionSuffix(): void { command('make:scaffold order -suffix'); - $dir = '\\' . DIRECTORY_SEPARATOR; - $migration = "APPPATH{$dir}Database{$dir}Migrations{$dir}(.*)\\.php"; - preg_match('/' . $migration . '/u', $this->getStreamFilterBuffer(), $matches); - $matches[0] = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $matches[0]); - // Files check $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); $this->assertFileExists(APPPATH . 'Controllers/OrderController.php'); $this->assertStringContainsString('_OrderMigration.php', $this->getStreamFilterBuffer()); $this->assertFileExists(APPPATH . 'Database/Seeds/OrderSeeder.php'); $this->assertFileExists(APPPATH . 'Models/OrderModel.php'); - - // Clean up - unlink(APPPATH . 'Controllers/OrderController.php'); - unlink($matches[0]); - unlink(APPPATH . 'Database/Seeds/OrderSeeder.php'); - unlink(APPPATH . 'Models/OrderModel.php'); } public function testCreateComponentWithOptionForce(): void @@ -161,11 +126,6 @@ public function testCreateComponentWithOptionForce(): void command('make:scaffold fixer -bare -force'); - $dir = '\\' . DIRECTORY_SEPARATOR; - $migration = "APPPATH{$dir}Database{$dir}Migrations{$dir}(.*)\\.php"; - preg_match('/' . $migration . '/u', $this->getStreamFilterBuffer(), $matches); - $matches[0] = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, $matches[0]); - // Files check $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); $this->assertFileExists(APPPATH . 'Controllers/Fixer.php'); @@ -176,12 +136,6 @@ public function testCreateComponentWithOptionForce(): void // Options check $this->assertStringContainsString('extends Controller', $this->getFileContents(APPPATH . 'Controllers/Fixer.php')); $this->assertStringContainsString('File overwritten: ', $this->getStreamFilterBuffer()); - - // Clean up - unlink(APPPATH . 'Controllers/Fixer.php'); - unlink($matches[0]); - unlink(APPPATH . 'Database/Seeds/Fixer.php'); - unlink(APPPATH . 'Models/Fixer.php'); } public function testCreateComponentWithOptionNamespace(): void @@ -205,11 +159,5 @@ public function testCreateComponentWithOptionNamespace(): void $this->assertStringContainsString('namespace App\Database\Migrations;', $this->getFileContents($matches[0])); $this->assertStringContainsString('namespace App\Database\Seeds;', $this->getFileContents(APPPATH . 'Database/Seeds/Product.php')); $this->assertStringContainsString('namespace App\Models;', $this->getFileContents(APPPATH . 'Models/Product.php')); - - // Clean up - unlink(APPPATH . 'Controllers/Product.php'); - unlink($matches[0]); - unlink(APPPATH . 'Database/Seeds/Product.php'); - unlink(APPPATH . 'Models/Product.php'); } } From 0f09214155626fd1ac37b86977175ccd759ac221 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 7 May 2026 03:36:37 +0200 Subject: [PATCH 8/8] fix: guard factories cache writes Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/Cache/FactoriesCache/FileVarExportHandler.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php index e22165827e7d..70456d9e1462 100644 --- a/system/Cache/FactoriesCache/FileVarExportHandler.php +++ b/system/Cache/FactoriesCache/FileVarExportHandler.php @@ -21,8 +21,8 @@ public function save(string $key, mixed $val): void { $val = var_export($val, true); - if (! is_dir($this->path)) { - mkdir($this->path, 0777, true); + if (! is_dir($this->path) && ! @mkdir($this->path, 0777, true) && ! is_dir($this->path)) { + return; } // Write to temp file first to ensure atomicity @@ -31,7 +31,9 @@ public function save(string $key, mixed $val): void return; } - rename($tmp, $this->path . "/{$key}"); + if (! @rename($tmp, $this->path . "/{$key}")) { + @unlink($tmp); + } } public function delete(string $key): void