markTestSkipped(self::$skipReason); } // 각 테스트 시작 시 lang-packs/_bundled 디렉토리 초기화 $bundle = self::$sharedBase.'/lang-packs/_bundled'; if (is_dir($bundle)) { foreach (scandir($bundle) as $e) { if ($e === '.' || $e === '..') { continue; } self::removeRecursiveStatic($bundle.DIRECTORY_SEPARATOR.$e); } } else { mkdir($bundle, 0755, true); } } #[Test] public function scan_returns_empty_when_directory_empty(): void { $result = scanLanguagePacks(); $this->assertSame([], $result); } #[Test] public function scan_includes_core_and_extension_packs(): void { $this->writeManifest('g7-core-ja', [ 'identifier' => 'g7-core-ja', 'scope' => 'core', 'target_identifier' => null, 'locale' => 'ja', 'locale_native_name' => '日本語', 'name' => ['ko' => 'G7 Core Japanese', 'en' => 'G7 Core Japanese'], 'version' => '1.0.0', ]); $this->writeManifest('g7-module-board-ja', [ 'identifier' => 'g7-module-board-ja', 'scope' => 'module', 'target_identifier' => 'sirsoft-board', 'locale' => 'ja', 'locale_native_name' => '日本語', 'name' => 'Board JP', 'version' => '1.0.0', ]); $result = scanLanguagePacks(); $ids = array_column($result, 'identifier'); sort($ids); $this->assertSame(['g7-core-ja', 'g7-module-board-ja'], $ids); $core = $this->findById($result, 'g7-core-ja'); $this->assertSame('core', $core['scope']); $this->assertNull($core['target_identifier']); $this->assertSame('ja', $core['locale']); $this->assertSame('日本語', $core['locale_native_name']); $module = $this->findById($result, 'g7-module-board-ja'); $this->assertSame('module', $module['scope']); $this->assertSame('sirsoft-board', $module['target_identifier']); } #[Test] public function scan_excludes_hidden_manifests(): void { $this->writeManifest('g7-sample-hidden-ja', [ 'identifier' => 'g7-sample-hidden-ja', 'scope' => 'core', 'locale' => 'ja', 'name' => 'Hidden Sample', 'version' => '1.0.0', 'hidden' => true, ]); $this->writeManifest('g7-core-ja', [ 'identifier' => 'g7-core-ja', 'scope' => 'core', 'locale' => 'ja', 'name' => 'G7 Core Japanese', 'version' => '1.0.0', ]); $result = scanLanguagePacks(); $ids = array_column($result, 'identifier'); $this->assertContains('g7-core-ja', $ids); $this->assertNotContains('g7-sample-hidden-ja', $ids); } #[Test] public function scan_supports_multiple_locales_for_same_extension(): void { $this->writeManifest('g7-module-board-ja', [ 'identifier' => 'g7-module-board-ja', 'scope' => 'module', 'target_identifier' => 'sirsoft-board', 'locale' => 'ja', 'name' => 'Board JP', 'version' => '1.0.0', ]); $this->writeManifest('g7-module-board-zh-CN', [ 'identifier' => 'g7-module-board-zh-CN', 'scope' => 'module', 'target_identifier' => 'sirsoft-board', 'locale' => 'zh-CN', 'name' => 'Board CN', 'version' => '1.0.0', ]); $result = scanLanguagePacks(); $this->assertCount(2, $result); $locales = array_column($result, 'locale'); sort($locales); $this->assertSame(['ja', 'zh-CN'], $locales); foreach ($result as $row) { $this->assertSame('sirsoft-board', $row['target_identifier']); } } #[Test] public function scan_excludes_packs_targeting_hidden_host_extensions(): void { // hidden 처리된 모듈 매니페스트 작성 (학습용 샘플 시뮬레이션) $modulesDir = self::$sharedBase.'/modules/_bundled/sample-hidden-module'; if (! is_dir($modulesDir)) { mkdir($modulesDir, 0755, true); } file_put_contents($modulesDir.'/module.json', json_encode([ 'identifier' => 'sample-hidden-module', 'name' => ['ko' => '샘플 모듈', 'en' => 'Sample Module'], 'version' => '1.0.0', 'hidden' => true, ], JSON_UNESCAPED_UNICODE)); // 일반 모듈 매니페스트 $normalDir = self::$sharedBase.'/modules/_bundled/sirsoft-board'; if (! is_dir($normalDir)) { mkdir($normalDir, 0755, true); } file_put_contents($normalDir.'/module.json', json_encode([ 'identifier' => 'sirsoft-board', 'name' => ['ko' => '게시판', 'en' => 'Board'], 'version' => '1.0.0', ], JSON_UNESCAPED_UNICODE)); // hidden 모듈 종속 언어팩 $this->writeManifest('g7-module-sample-hidden-module-ja', [ 'identifier' => 'g7-module-sample-hidden-module-ja', 'scope' => 'module', 'target_identifier' => 'sample-hidden-module', 'locale' => 'ja', 'name' => 'Sample Hidden JP', 'version' => '1.0.0', ]); // 정상 모듈 종속 언어팩 $this->writeManifest('g7-module-sirsoft-board-ja', [ 'identifier' => 'g7-module-sirsoft-board-ja', 'scope' => 'module', 'target_identifier' => 'sirsoft-board', 'locale' => 'ja', 'name' => 'Board JP', 'version' => '1.0.0', ]); $result = scanLanguagePacks(); $ids = array_column($result, 'identifier'); $this->assertContains('g7-module-sirsoft-board-ja', $ids); $this->assertNotContains('g7-module-sample-hidden-module-ja', $ids, 'hidden 호스트 확장 종속 언어팩은 cascade 제외되어야 함'); // fixture 정리 @unlink($modulesDir.'/module.json'); @rmdir($modulesDir); @unlink($normalDir.'/module.json'); @rmdir($normalDir); } #[Test] public function scan_skips_directory_without_manifest(): void { mkdir(self::$sharedBase.'/lang-packs/_bundled/g7-orphan-ja', 0755, true); $this->writeManifest('g7-core-ja', [ 'identifier' => 'g7-core-ja', 'scope' => 'core', 'locale' => 'ja', 'name' => 'G7 Core Japanese', 'version' => '1.0.0', ]); $result = scanLanguagePacks(); $this->assertCount(1, $result); $this->assertSame('g7-core-ja', $result[0]['identifier']); } private function writeManifest(string $directory, array $data): void { $dir = self::$sharedBase.'/lang-packs/_bundled/'.$directory; if (! is_dir($dir)) { mkdir($dir, 0755, true); } file_put_contents($dir.'/language-pack.json', json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); } private function findById(array $rows, string $id): array { foreach ($rows as $row) { if ($row['identifier'] === $id) { return $row; } } $this->fail("Identifier {$id} not found in scan result"); } private static function removeRecursiveStatic(string $path): void { if (! file_exists($path) && ! is_link($path)) { return; } if (is_file($path) || is_link($path)) { @unlink($path); return; } $entries = scandir($path); foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') { continue; } self::removeRecursiveStatic($path.DIRECTORY_SEPARATOR.$entry); } @rmdir($path); } }