basePath = $root.DIRECTORY_SEPARATOR.'base'; $this->theirsPath = $root.DIRECTORY_SEPARATOR.'theirs'; File::ensureDirectoryExists($this->basePath); File::ensureDirectoryExists($this->theirsPath); } protected function tearDown(): void { $root = dirname($this->basePath); if (File::isDirectory($root)) { File::deleteDirectory($root); } parent::tearDown(); } /** * base == theirs 인 파일은 적용 목록에서 제외됩니다 (코어 미변경 → 보존). */ public function test_unchanged_file_is_excluded_from_apply_list(): void { $this->writeBoth('app/Same.php', 'basePath, $this->theirsPath, ['app'], [], [], ); $this->assertNotContains('app/Same.php', $result['apply']); $this->assertSame(0, $result['added_count']); $this->assertSame(0, $result['changed_count']); } /** * base 에 없고 theirs 에만 있는 파일은 added 로 포함됩니다 (신규). */ public function test_new_file_is_added_to_apply_list(): void { File::ensureDirectoryExists($this->theirsPath.DIRECTORY_SEPARATOR.'app'); File::put($this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'New.php', 'basePath, $this->theirsPath, ['app'], [], [], ); $this->assertContains('app/New.php', $result['apply']); $this->assertSame(1, $result['added_count']); $this->assertSame(0, $result['changed_count']); } /** * size 가 다른 파일은 md5 계산 없이 changed 로 포함됩니다. */ public function test_size_differing_file_is_changed(): void { $this->writeBoth('app/Grown.php', 'basePath, $this->theirsPath, ['app'], [], [], ); $this->assertContains('app/Grown.php', $result['apply']); $this->assertSame(1, $result['changed_count']); $this->assertSame(0, $result['added_count']); } /** * size 는 같지만 내용(md5)이 다른 파일은 changed 로 포함됩니다. */ public function test_same_size_different_content_file_is_changed(): void { // 동일 길이(12), 내용만 다름 $this->writeBoth('app/Edited.php', 'AAAAAAAAAAAA', 'BBBBBBBBBBBB'); $result = CoreBackupHelper::computeApplyList( $this->basePath, $this->theirsPath, ['app'], [], [], ); $this->assertContains('app/Edited.php', $result['apply']); $this->assertSame(1, $result['changed_count']); } /** * excludes 패턴에 매칭되는 파일은 목록에서 제외됩니다. */ public function test_excluded_file_is_not_in_apply_list(): void { // theirs 에만 존재하지만 node_modules 하위 → 제외 File::ensureDirectoryExists($this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'node_modules'); File::put($this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'node_modules'.DIRECTORY_SEPARATOR.'x.js', 'x'); $result = CoreBackupHelper::computeApplyList( $this->basePath, $this->theirsPath, ['app'], [], ['node_modules'], ); $this->assertSame([], $result['apply']); } /** * protected_paths 하위 항목은 목록에서 제외됩니다. */ public function test_protected_path_file_is_not_in_apply_list(): void { File::ensureDirectoryExists($this->theirsPath.DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'); File::put($this->theirsPath.DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'runtime.txt', 'data'); $result = CoreBackupHelper::computeApplyList( $this->basePath, $this->theirsPath, ['storage'], ['storage'], [], ); $this->assertSame([], $result['apply']); } /** * added 와 changed 가 섞여 있을 때 apply 목록이 정렬되어 둘 다 포함합니다. */ public function test_apply_list_merges_added_and_changed_sorted(): void { $this->writeBoth('app/Zeta.php', 'old', 'new-different'); // changed File::put($this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Alpha.php', 'brand-new'); // added $this->writeBoth('app/Keep.php', 'same', 'same'); // unchanged → 제외 $result = CoreBackupHelper::computeApplyList( $this->basePath, $this->theirsPath, ['app'], [], [], ); $this->assertSame(['app/Alpha.php', 'app/Zeta.php'], $result['apply']); $this->assertSame(1, $result['added_count']); $this->assertSame(1, $result['changed_count']); } /** * 단일 파일 target 도 3-way 판정을 받습니다 (변경 시 changed). */ public function test_single_file_target_is_classified(): void { File::put($this->basePath.DIRECTORY_SEPARATOR.'artisan', 'v1'); File::put($this->theirsPath.DIRECTORY_SEPARATOR.'artisan', 'v2-changed'); $result = CoreBackupHelper::computeApplyList( $this->basePath, $this->theirsPath, ['artisan'], [], [], ); $this->assertContains('artisan', $result['apply']); $this->assertSame(1, $result['changed_count']); } /** * theirs 의 symlink 는 적용 목록에서 제외되고 has_symlink 플래그가 켜집니다. * (링크 자체는 copyDirectory 의 별도 링크 처리 경로가 담당 — 목록 산출 대상 아님) */ public function test_symlink_is_excluded_and_flagged(): void { // theirs/app/real.php + theirs/app/link.php(→ real.php) 구성 File::ensureDirectoryExists($this->theirsPath.DIRECTORY_SEPARATOR.'app'); $realTarget = $this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'real.php'; File::put($realTarget, 'theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'link.php'; if (! @symlink($realTarget, $linkPath) || ! is_link($linkPath)) { // Windows SeCreateSymbolicLink 권한 부족 등 — 링크 생성 불가 환경은 skip $this->markTestSkipped('symlink 생성 불가 환경 (권한 부족)'); } $result = CoreBackupHelper::computeApplyList( $this->basePath, $this->theirsPath, ['app'], [], [], ); // real.php 는 added 로 포함되지만 link.php 는 목록에서 제외 $this->assertContains('app/real.php', $result['apply']); $this->assertNotContains('app/link.php', $result['apply']); $this->assertTrue($result['has_symlink'], 'symlink 발견 시 has_symlink 플래그가 켜져야 한다'); } /** * base·theirs 양쪽에 동일 파일을 씁니다. */ private function writeBoth(string $relative, string $baseContent, string $theirsContent): void { $rel = str_replace('/', DIRECTORY_SEPARATOR, $relative); $baseFile = $this->basePath.DIRECTORY_SEPARATOR.$rel; File::ensureDirectoryExists(dirname($baseFile)); File::put($baseFile, $baseContent); $theirsFile = $this->theirsPath.DIRECTORY_SEPARATOR.$rel; File::ensureDirectoryExists(dirname($theirsFile)); File::put($theirsFile, $theirsContent); } }