Files
Gnuboard7/tests/Unit/Extension/CoreBackupHelperApplyListTest.php
T
HeuJung d0943d6cb6 feat(core): core:update 기본을 코어 변경분만 적용(3-way skip)으로 전환
core:update Step 7 이 targets 전체를 무조건 재복사하고 orphan 을 삭제하여
사용자 .htaccess 커스텀 블록·_bundled 커스텀 확장이 소실되던 사고(공개 )를
차단한다. 백업(base)↔_pending(theirs) 3-way md5 비교로 코어가 실제 변경/추가한
파일만 산출해 적용하고, 코어 미변경 파일은 복사·권한·mtime 을 전부 스킵해 현재
디스크 상태(사용자 수정 포함)를 보존한다. 전체 덮어쓰기 + orphan 정리가 필요하면
--prune 으로 opt-in. 백업 부재(--no-backup) 시 base 가 없어 전체 덮어쓰기로 안전
회귀 + 안내를 출력한다.

기존 stale 정리 테스트는 새 기본(증분)에서 orphan 을 삭제하지 않으므로 prune
컨텍스트로 전환했고, 증분 3-way 판정·목록 적용·fallback 단위 테스트와 --prune
커맨드 표면 테스트를 추가했다.

번들 ja 언어팩은 코어 apply_mode 안내 키(g7-core-ja)와 함께, 같은 빌드에서
동기화된 관리자↔유저 크로스링크 라벨(board/ecommerce/admin_basic/basic)의 일본어
번역을 반영하고 각 패키지 버전·CHANGELOG 를 갱신했다.
2026-07-07 22:45:16 +09:00

255 lines
8.8 KiB
PHP

<?php
namespace Tests\Unit\Extension;
use App\Extension\Helpers\CoreBackupHelper;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
/**
* CoreBackupHelper::computeApplyList() 단위 테스트 (공개 #64)
*
* `core:update` 기본(증분) 모드의 3-way 판정을 검증합니다:
* - base == theirs (코어 미변경) → 적용 목록 제외 (사용자 상태 보존)
* - base 없음 (신규) → added 로 목록 포함
* - size 다름 / md5 다름 (변경) → changed 로 목록 포함
* - symlink / excludes / protected → 목록 제외
*/
class CoreBackupHelperApplyListTest extends TestCase
{
private string $basePath; // 백업 스냅샷 (base = 구버전 원본)
private string $theirsPath; // _pending (theirs = 신 버전)
protected function setUp(): void
{
parent::setUp();
$root = storage_path('app/test_apply_list_'.uniqid());
$this->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', '<?php // identical', '<?php // identical');
$result = CoreBackupHelper::computeApplyList(
$this->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', '<?php // new');
$result = CoreBackupHelper::computeApplyList(
$this->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', '<?php // small', '<?php // this content is clearly longer than base');
$result = CoreBackupHelper::computeApplyList(
$this->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, '<?php // real');
$linkPath = $this->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);
}
}