testRoot = storage_path('app/test_core_prune_'.uniqid()); $this->backupPath = $this->testRoot.'/backup'; $this->sourcePath = $this->testRoot.'/_pending'; $this->activePath = $this->testRoot.'/active'; File::ensureDirectoryExists($this->backupPath); File::ensureDirectoryExists($this->sourcePath); File::ensureDirectoryExists($this->activePath); } protected function tearDown(): void { if (File::isDirectory($this->testRoot)) { File::deleteDirectory($this->testRoot); } parent::tearDown(); } // ======================================================================== // writeNewFilesManifest() — manifest 생성 // ======================================================================== /** * _pending 에만 있고 backup 에는 없는 파일이 new_files 로 식별되는지 검증. */ public function test_write_manifest_identifies_new_files_in_pending_only(): void { // backup (= 활성 디렉토리의 사전 스냅샷) 에 있는 기존 파일 File::ensureDirectoryExists($this->backupPath.'/app/Services'); File::put($this->backupPath.'/app/Services/ExistingService.php', 'sourcePath.'/app/Services'); File::put($this->sourcePath.'/app/Services/ExistingService.php', 'sourcePath.'/app/Services/NewServiceProviderClass.php', 'backupPath, $this->sourcePath, ['app'], [], [], '7.0.0-beta.5', '7.0.0-beta.6', ); $manifestPath = $this->backupPath.'/_new_files_manifest.json'; $this->assertFileExists($manifestPath); $manifest = json_decode(File::get($manifestPath), true); $this->assertIsArray($manifest); $this->assertSame(1, $manifest['version']); $this->assertSame('7.0.0-beta.5', $manifest['from_version']); $this->assertSame('7.0.0-beta.6', $manifest['to_version']); $this->assertContains('app/Services/NewServiceProviderClass.php', $manifest['new_files']); $this->assertNotContains('app/Services/ExistingService.php', $manifest['new_files']); $this->assertGreaterThanOrEqual(1, $result['new_files_count']); } /** * _pending 에만 있는 신규 디렉토리가 new_dirs 로 식별되는지 검증. */ public function test_write_manifest_identifies_new_dirs(): void { File::ensureDirectoryExists($this->backupPath.'/app'); File::put($this->backupPath.'/app/keep.php', 'sourcePath.'/app'); File::put($this->sourcePath.'/app/keep.php', 'sourcePath.'/app/Services/LanguagePack'); File::put($this->sourcePath.'/app/Services/LanguagePack/Module.php', 'backupPath, $this->sourcePath, ['app'], [], [], '7.0.0-beta.5', '7.0.0-beta.6', ); $manifest = json_decode(File::get($this->backupPath.'/_new_files_manifest.json'), true); $this->assertContains('app/Services/LanguagePack', $manifest['new_dirs']); $this->assertContains('app/Services/LanguagePack/Module.php', $manifest['new_files']); } /** * protectedPaths 하위는 manifest 에서 제외되는지 검증 (방어 깊이). */ public function test_write_manifest_excludes_protected_paths(): void { // _pending 에 storage/.env 가상 신규 파일 — 보호 경로 File::ensureDirectoryExists($this->sourcePath.'/storage'); File::put($this->sourcePath.'/storage/secret.txt', 'should not be tracked'); File::ensureDirectoryExists($this->sourcePath.'/app'); File::put($this->sourcePath.'/app/Real.php', 'backupPath, $this->sourcePath, ['app', 'storage'], ['storage'], [], '7.0.0-beta.5', '7.0.0-beta.6', ); $manifest = json_decode(File::get($this->backupPath.'/_new_files_manifest.json'), true); $this->assertNotContains('storage/secret.txt', $manifest['new_files']); $this->assertContains('app/Real.php', $manifest['new_files']); } /** * excludes 패턴 (node_modules, .git 등) 은 manifest 에서 제외. */ public function test_write_manifest_excludes_pattern_matches(): void { File::ensureDirectoryExists($this->sourcePath.'/app/node_modules'); File::put($this->sourcePath.'/app/node_modules/lib.js', ''); File::ensureDirectoryExists($this->sourcePath.'/app'); File::put($this->sourcePath.'/app/Real.php', 'backupPath, $this->sourcePath, ['app'], [], ['node_modules'], '7.0.0-beta.5', '7.0.0-beta.6', ); $manifest = json_decode(File::get($this->backupPath.'/_new_files_manifest.json'), true); $this->assertNotContains('app/node_modules/lib.js', $manifest['new_files']); $this->assertContains('app/Real.php', $manifest['new_files']); } /** * manifest JSON 의 키 순서 / 정렬 규칙 invariant — beta.6 Upgrade DataMigration 01 * 의 사후 작성본과 바이트 단위 호환을 보장하기 위해 키 순서/정렬을 고정한다. */ public function test_manifest_schema_invariant(): void { File::ensureDirectoryExists($this->sourcePath.'/app'); File::put($this->sourcePath.'/app/Beta.php', 'sourcePath.'/app/Alpha.php', 'backupPath, $this->sourcePath, ['app'], [], [], '7.0.0-beta.5', '7.0.0-beta.6', ); $manifest = json_decode(File::get($this->backupPath.'/_new_files_manifest.json'), true); // 정렬 규칙: new_files / new_dirs 는 lexicographic ascending $sorted = $manifest['new_files']; $copy = $sorted; sort($copy, SORT_STRING); $this->assertSame($copy, $sorted, 'new_files 는 알파벳 오름차순 정렬되어야 함'); // 최상위 키 6종 명시 (DataMigration 01 의 invariant) $this->assertSame( ['version', 'created_at', 'from_version', 'to_version', 'new_files', 'new_dirs'], array_keys($manifest), ); } // ======================================================================== // pruneNewFiles() — manifest 기반 신규 파일 정리 // ======================================================================== /** * manifest 에 등록된 신규 파일이 활성 디렉토리에서 삭제되는지 검증. */ public function test_prune_removes_new_files_listed_in_manifest(): void { File::ensureDirectoryExists($this->activePath.'/app/Services/LanguagePack'); File::put($this->activePath.'/app/Services/LanguagePack/Module.php', 'activePath.'/app/Services/UserKeep.php', 'writeManifest([ 'new_files' => ['app/Services/LanguagePack/Module.php'], 'new_dirs' => ['app/Services/LanguagePack'], ]); $result = CoreBackupHelper::pruneNewFiles($this->backupPath, $this->activePath, []); $this->assertFileDoesNotExist($this->activePath.'/app/Services/LanguagePack/Module.php'); $this->assertDirectoryDoesNotExist($this->activePath.'/app/Services/LanguagePack'); // 사용자 파일은 보존 $this->assertFileExists($this->activePath.'/app/Services/UserKeep.php'); $this->assertSame(1, $result['removed_files']); $this->assertSame(1, $result['removed_dirs']); } /** * manifest 의 new_dirs 중 사용자 파일이 남아있는 디렉토리는 rmdir 안 함. */ public function test_prune_keeps_dirs_with_user_files(): void { File::ensureDirectoryExists($this->activePath.'/app/Services/LanguagePack'); File::put($this->activePath.'/app/Services/LanguagePack/Module.php', 'activePath.'/app/Services/LanguagePack/UserAdded.php', 'writeManifest([ 'new_files' => ['app/Services/LanguagePack/Module.php'], 'new_dirs' => ['app/Services/LanguagePack'], ]); $result = CoreBackupHelper::pruneNewFiles($this->backupPath, $this->activePath, []); $this->assertFileDoesNotExist($this->activePath.'/app/Services/LanguagePack/Module.php'); $this->assertDirectoryExists($this->activePath.'/app/Services/LanguagePack'); $this->assertFileExists($this->activePath.'/app/Services/LanguagePack/UserAdded.php'); $this->assertSame(1, $result['removed_files']); $this->assertSame(0, $result['removed_dirs']); } /** * protected_paths 하위 manifest 항목은 prune 시점에 재검증되어 삭제되지 않음. */ public function test_prune_respects_protected_paths_double_guard(): void { // manifest 에 가상으로 보호 경로 항목을 주입 — 운영 결함 또는 악의적 매니페스트 시뮬레이션 File::ensureDirectoryExists($this->activePath.'/storage'); File::put($this->activePath.'/storage/secret.txt', 'must keep'); $this->writeManifest([ 'new_files' => ['storage/secret.txt'], 'new_dirs' => [], ]); $result = CoreBackupHelper::pruneNewFiles( $this->backupPath, $this->activePath, ['storage'], ); $this->assertFileExists($this->activePath.'/storage/secret.txt'); $this->assertSame(0, $result['removed_files']); $this->assertGreaterThanOrEqual(1, $result['protected_count']); } /** * 활성 디렉토리에 symlink 가 있고 manifest 에 등재되어 있어도 절대 삭제하지 않음. * Windows 에서는 markTestSkipped (symlink() 권한 제약). */ public function test_prune_skips_symlinks_unconditionally(): void { if (PHP_OS_FAMILY === 'Windows') { $this->markTestSkipped('Windows 에서는 PHP symlink() 권한이 필요하여 건너뜀'); } File::ensureDirectoryExists($this->activePath.'/public'); File::ensureDirectoryExists($this->activePath.'/storage/app/public'); symlink($this->activePath.'/storage/app/public', $this->activePath.'/public/storage'); $this->writeManifest([ 'new_files' => ['public/storage'], 'new_dirs' => [], ]); $result = CoreBackupHelper::pruneNewFiles($this->backupPath, $this->activePath, []); $this->assertTrue(is_link($this->activePath.'/public/storage')); $this->assertGreaterThanOrEqual(1, $result['symlink_skipped']); } /** * manifest 부재 시 prune 은 noop + warning 없이 빈 결과 반환. */ public function test_prune_returns_zero_when_manifest_absent(): void { File::put($this->activePath.'/keep.php', 'backupPath, $this->activePath, []); $this->assertFileExists($this->activePath.'/keep.php'); $this->assertSame(0, $result['removed_files']); $this->assertSame(0, $result['removed_dirs']); $this->assertFalse($result['manifest_loaded']); } /** * manifest JSON 파싱 실패 시 prune 은 noop + manifest_loaded:false. */ public function test_prune_handles_corrupted_manifest_gracefully(): void { File::put($this->backupPath.'/_new_files_manifest.json', '{this is not valid json'); File::put($this->activePath.'/keep.php', 'backupPath, $this->activePath, []); $this->assertFileExists($this->activePath.'/keep.php'); $this->assertFalse($result['manifest_loaded']); } /** * manifest 의 new_files 가 실제 디스크에 없으면 (이미 삭제됨) skip. */ public function test_prune_skips_missing_files_gracefully(): void { $this->writeManifest([ 'new_files' => ['app/Nonexistent.php'], 'new_dirs' => ['app/Ghost'], ]); $result = CoreBackupHelper::pruneNewFiles($this->backupPath, $this->activePath, []); $this->assertSame(0, $result['removed_files']); $this->assertSame(0, $result['removed_dirs']); } /** * 빈 디렉토리 정리 — new_dirs 가 깊이 역순으로 처리되어 중첩 디렉토리도 제거. */ public function test_prune_empty_dirs_in_depth_reverse_order(): void { File::ensureDirectoryExists($this->activePath.'/a/b/c'); File::put($this->activePath.'/a/b/c/file.php', 'writeManifest([ 'new_files' => ['a/b/c/file.php'], 'new_dirs' => ['a', 'a/b', 'a/b/c'], ]); $result = CoreBackupHelper::pruneNewFiles($this->backupPath, $this->activePath, []); $this->assertDirectoryDoesNotExist($this->activePath.'/a'); $this->assertSame(3, $result['removed_dirs']); } private function writeManifest(array $data): void { $manifest = [ 'version' => 1, 'created_at' => date('c'), 'from_version' => '7.0.0-beta.5', 'to_version' => '7.0.0-beta.6', 'new_files' => $data['new_files'] ?? [], 'new_dirs' => $data['new_dirs'] ?? [], ]; File::put($this->backupPath.'/_new_files_manifest.json', json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, )); } }